{"openapi":"3.0.3","info":{"title":"Canadian Political Data — Public API","description":"Read-only access to the public Canadian political dataset: politicians, jurisdictions, coverage stats. Bearer-token authenticated via API keys minted at /account/api-keys. See /developers for the full guide.","version":"1.0.0","contact":{"name":"Canadian Political Data","url":"https://canadianpoliticaldata.org/","email":"admin@thebunkerops.ca"},"license":{"name":"See repository LICENSE","url":"https://canadianpoliticaldata.org/about/"}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"cpd_<env>_<random>_<checksum>","description":"API keys minted at /account/api-keys. Format: cpd_live_<22-char-base62>_<6-char-checksum>. Anonymous calls work too, at a lower rate limit (30/hr per IP)."}},"schemas":{}},"paths":{"/search/speeches":{"get":{"summary":"Hybrid HNSW + BM25 semantic search over Hansard","tags":["Search (semantic)"],"description":"Mirror of the internal /api/v1/search/speeches endpoint. Available to every authenticated tier — free=5/hr, dev=100/hr, pro=10000/hr — counted against a SEPARATE bucket from the general API rate limit (so semantic queries don't drain the 60/hr free or 1000/hr dev budget for /coverage etc., and vice versa). The embed step also routes through a shared TEI semaphore (max 2 concurrent + 6 queued; 503 with Retry-After if the queue saturates) — that GPU-protection layer is orthogonal to the per-tier rate limit. Same response shape as the internal route (timeline mode by default; group_by=politician for grouped). See /developers/rate-limiting for both layers.","parameters":[{"schema":{"type":"string","maxLength":500},"in":"query","name":"q","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"anchor_chunk_id","required":false},{"schema":{"type":"string","enum":["en","fr","any"]},"in":"query","name":"lang","required":false},{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"array","items":{"type":"string","format":"uuid"}}]},"in":"query","name":"politician_ids","required":false},{"schema":{"type":"string"},"in":"query","name":"party","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string"}]},"in":"query","name":"exclude_presiding","required":false},{"schema":{"type":"string","enum":["active","inactive"]},"in":"query","name":"politician_active","required":false},{"schema":{"type":"number","minimum":0,"maximum":1},"in":"query","name":"min_similarity","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"parliament_number","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"session_number","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"speech_type","required":false},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":50},"in":"query","name":"limit","required":false},{"schema":{"type":"string","enum":["timeline","politician"]},"in":"query","name":"group_by","required":false},{"schema":{"type":"integer","minimum":1,"maximum":10},"in":"query","name":"per_group_limit","required":false},{"schema":{"type":"string","enum":["mentions","best_match","avg_match","keyword_hits"]},"in":"query","name":"sort","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false"]}]},"in":"query","name":"include_count","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/search/speeches/count":{"get":{"summary":"Count-only sibling for /search/speeches","tags":["Search (semantic)"],"description":"Returns { total, capped }. Capping kicks in at 10,000 + 1 (HNSW LIMIT trick). Use alongside ?include_count=false on /search/speeches to stage count off the hot path. Shares the semantic-search rate-limit bucket (free=5/hr, dev=100/hr, pro=10000/hr) and the TEI semaphore with /search/speeches.","parameters":[{"schema":{"type":"string","maxLength":500},"in":"query","name":"q","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"anchor_chunk_id","required":false},{"schema":{"type":"string","enum":["en","fr","any"]},"in":"query","name":"lang","required":false},{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"array","items":{"type":"string","format":"uuid"}}]},"in":"query","name":"politician_ids","required":false},{"schema":{"type":"string"},"in":"query","name":"party","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string"}]},"in":"query","name":"exclude_presiding","required":false},{"schema":{"type":"string","enum":["active","inactive"]},"in":"query","name":"politician_active","required":false},{"schema":{"type":"number","minimum":0,"maximum":1},"in":"query","name":"min_similarity","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"parliament_number","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"session_number","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"speech_type","required":false},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":50},"in":"query","name":"limit","required":false},{"schema":{"type":"string","enum":["timeline","politician"]},"in":"query","name":"group_by","required":false},{"schema":{"type":"integer","minimum":1,"maximum":10},"in":"query","name":"per_group_limit","required":false},{"schema":{"type":"string","enum":["mentions","best_match","avg_match","keyword_hits"]},"in":"query","name":"sort","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false"]}]},"in":"query","name":"include_count","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/search/facets":{"get":{"summary":"Aggregations over the top-N candidate pool","tags":["Search (semantic)"],"description":"Returns { analyzed_count, analysis_limit, chunk_ids, by_party, by_politician, by_year, by_language, keyword_overlap, mode }. Optional ?limit query (clamped [10, 500], default 200) sets the candidate-pool size. Shares the semantic-search rate-limit bucket and the TEI semaphore with /search/speeches.","parameters":[{"schema":{"type":"string","maxLength":500},"in":"query","name":"q","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"anchor_chunk_id","required":false},{"schema":{"type":"integer","minimum":10,"maximum":500},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/search/sessions":{"get":{"summary":"Parliament + session catalog (FREE)","tags":["Search (free)"],"description":"Returns { sessions: [{ parliament_number, session_number, name, start_date, end_date }] }. Backs the cascading dropdown on the search filter UI. Cache-Control: public, max-age=3600.","parameters":[{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/search/chunks/{id}":{"get":{"summary":"Anchor-chunk lookup by UUID (FREE)","tags":["Search (free)"],"description":"Returns the chunk text + speech metadata + politician (if resolved). 404 on missing or malformed id. Cache-Control: public, max-age=60.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/search/meta":{"get":{"summary":"Backfill-progress meta (FREE)","tags":["Search (free)"],"description":"Returns { total_chunks, embedded_chunks, coverage }. Useful for callers wanting to know what fraction of the corpus is currently embedded + searchable.","responses":{"200":{"description":"Default Response"}}}},"/bills":{"get":{"summary":"List bills with jurisdiction + session + status filters","tags":["Bills"],"description":"Paginated list of bills across federal + provincial legislatures. Filters: level, province_territory, session_id, status, sponsor_politician_id, q (substring of title/short_title/bill_number), introduced_from, introduced_to. Each item includes the session label, denormalized events_count + sponsors_count, and the upstream source_url. Response: `{ items: [...], page, limit, total, pages }`.","parameters":[{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"session_id","required":false},{"schema":{"type":"string"},"in":"query","name":"status","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"sponsor_politician_id","required":false},{"schema":{"type":"string","maxLength":200},"in":"query","name":"q","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"introduced_from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"introduced_to","required":false},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/bills/{id}":{"get":{"summary":"Single bill with session detail and denorm counts","tags":["Bills"],"description":"Full bill row joined to its legislative_session, plus events_count, sponsors_count, and votes_count. 404 on missing id. Response: `{ bill: { ... } }`.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/bills/{id}/events":{"get":{"summary":"Stage-transition events for one bill","tags":["Bills"],"description":"Append-only event log: first_reading, second_reading, committee, third_reading, royal_assent, etc., ordered by event_date ASC. Federal events come from parl.ca/LegisInfo XML; provincial events come from per-jurisdiction Hansard + bills-status pages. Response: `{ bill_id, events: [...] }`.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/bills/{id}/sponsors":{"get":{"summary":"Sponsor list for one bill (FK-joined to politicians)","tags":["Bills"],"description":"Bill sponsors with role ('sponsor' | 'co_sponsor') and ordering. When politician_id is resolved, the politician's current name, party, and openparliament_slug are surfaced. When unresolved, sponsor_name_raw still carries the upstream string. Response: `{ bill_id, sponsors: [...] }`.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/votes":{"get":{"summary":"List recorded chamber votes across federal + provinces","tags":["Votes"],"description":"Paginated list of votes (divisions, voice votes, acclamations, and NT/NU consensus events). Federal data: 100% politician-FK via openparliament_slug. Provincial: division-or-consensus shape varies by jurisdiction. Filters: level, province_territory, session_id, bill_id, result, vote_type, occurred_from, occurred_to. Each item carries denormalized session + bill labels and a positions_count (use /votes/:id/positions for the breakdown). Response: `{ items: [...], page, limit, total, pages }`.","parameters":[{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"session_id","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"query","name":"bill_id","required":false},{"schema":{"type":"string","enum":["passed","defeated","tied","withdrawn","deferred"]},"in":"query","name":"result","required":false},{"schema":{"type":"string","enum":["division","voice","acclamation","consensus"]},"in":"query","name":"vote_type","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"occurred_from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"occurred_to","required":false},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/votes/{id}":{"get":{"summary":"Single vote with motion text and tallies","tags":["Votes"],"description":"Full vote row with motion_text, ayes/nays/abstentions, result, occurred_at, and bill linkage. 404 on missing id. Response: `{ vote: { ... } }`.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/votes/{id}/positions":{"get":{"summary":"Per-politician position breakdown for one vote","tags":["Votes"],"description":"Returns one row per politician who voted, with position ('yea' | 'nay' | 'abstain' | 'paired' | 'absent'), the party they belonged to at the time of the vote, and FK-joined politician name + current party + openparliament_slug. May be empty for voice / acclamation / consensus vote types. Not paginated — vote_positions is bounded per-vote (~350 rows even for federal). Response: `{ vote_id, positions: [...] }`.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/committees/meetings":{"get":{"summary":"Distinct committee meetings derived from Hansard speeches","tags":["Committees"],"description":"Paginated list of distinct committee meetings inferred from speeches WHERE speech_type='committee'. Federal meetings come from openparliament.ca; AB meetings from assembly.ab.ca PDF transcripts. Each item: `{ meeting_url, level, province_territory, source_system, date, first_spoken_at, last_spoken_at, speech_count }`. Filters: level, province_territory, source_system, from, to. For full-text search inside committee transcripts use `/api/public/v1/search/speeches?speech_type=committee` (pro-tier — TEI-dependent).","parameters":[{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"type":"string","maxLength":64},"in":"query","name":"source_system","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/politicians/{id}/socials":{"get":{"summary":"Social media handles for one politician","tags":["Politician socials"],"description":"List of social-media handles tied to this politician across platforms (Twitter, Bluesky, Mastodon, Instagram, Facebook, YouTube, TikTok, LinkedIn, Threads). Filters to handles where is_live=true by default; pass ?include_dead=true to include dead/abandoned handles for historical research. 404 on missing politician id. Response: `{ items: [{ platform, handle, url, follower_count, lifetime_post_count, last_post_at, last_profile_check_at, last_verified_at, is_live }, ...] }`. Cache-Control: public, max-age=300.","parameters":[{"schema":{"type":"boolean"},"in":"query","name":"include_dead","required":false,"description":"If true, include handles where is_live=false. Default: only live handles."},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true,"description":"UUID of the politician"}],"responses":{"200":{"description":"Default Response"}}}},"/politicians/{id}/posts":{"get":{"summary":"Scraped social posts for one politician","tags":["Politician socials"],"description":"Public-record posts captured by the paid scrape-monitoring pipeline. Filter by ?platform=twitter,bluesky (comma-separated). Limit capped at 200, default 50. Each post carries an optional `funded_by` field (opt-in attribution from the subscriber whose scrape captured it) and `funded_by_url` (optional link). Response: `{ items: [{ id, politician_id, platform, post_id, posted_at, text, url, media_urls, engagement, scraped_at, funded_by, funded_by_url }, ...] }`. Cache-Control: public, max-age=60.","parameters":[{"schema":{"type":"string","maxLength":200},"in":"query","name":"platform","required":false,"description":"Comma-separated platform filter, e.g. twitter,bluesky. Allowed values: twitter, bluesky, mastodon, instagram, facebook, youtube, tiktok, linkedin, threads."},{"schema":{"type":"integer","minimum":1,"maximum":200},"in":"query","name":"limit","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true,"description":"UUID of the politician"}],"responses":{"200":{"description":"Default Response"}}}},"/boundaries":{"get":{"summary":"Paginated list of constituency boundaries (metadata only)","tags":["Constituency boundaries"],"description":"List of federal/provincial/municipal electoral boundaries mirrored from Open North (represent.opennorth.ca). Metadata only — no GeoJSON payload; clients fetch detail by source_set + slug when they need geometry. Filters: level, province_territory (two-letter code; ignored for federal since federal ridings span all of Canada), bbox (minLng,minLat,maxLng,maxLat WGS84). Pagination: ?page=&limit= (limit cap 100, default 50). Response: `{ items: [{ constituency_id, name, level, province_territory, source_set, area_sqkm, centroid: { lng, lat }, effective_from, effective_to, boundaries_version }, ...], page, limit, total, pages }`. Cache-Control: public, max-age=3600.","parameters":[{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false},{"schema":{"type":"string","minLength":2,"maxLength":2},"in":"query","name":"province_territory","required":false},{"schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"},"in":"query","name":"bbox","required":false,"description":"Bounding-box filter as minLng,minLat,maxLng,maxLat (WGS84). Returns boundaries whose simplified geometry's bbox intersects."},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/boundaries/lookup":{"get":{"summary":"Find the riding(s) containing a lat/lng point or postcode","tags":["Constituency boundaries"],"description":"Point-in-polygon lookup against the full (non-simplified) boundary geometry. Returns the containing riding at each level by default — useful for 'who's my MP, MLA, and city councillor' civic-app patterns. Pass ?level=federal|provincial|municipal to narrow to one level. Two input modes: pass either ?lat=&lng= (WGS84; lat 40–85, lng -145 to -50) OR ?postcode= (6-char Canadian postcode like K1A0A6, or 3-char FSA like K1A). When postcode is passed it's resolved via Open North in real time and the centroid is used; if both are passed, postcode wins. Each match includes the simplified GeoJSON so a downstream map widget can render the riding without a follow-up call. Response: `{ federal: {...}|null, provincial: {...}|null, municipal: {...}|null }`. Cache-Control: public, max-age=3600.","parameters":[{"schema":{"type":"number","minimum":40,"maximum":85},"in":"query","name":"lat","required":false,"description":"Latitude in WGS84"},{"schema":{"type":"number","minimum":-145,"maximum":-50},"in":"query","name":"lng","required":false,"description":"Longitude in WGS84"},{"schema":{"type":"string","minLength":3,"maxLength":8},"in":"query","name":"postcode","required":false,"description":"Canadian postcode (6-char like K1A0A6) or FSA (3-char like K1A). Used as an alternative to lat/lng; if both are provided, postcode wins."},{"schema":{"type":"string","enum":["federal","provincial","municipal"]},"in":"query","name":"level","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/boundaries/{source_set}/{slug}":{"get":{"summary":"Single boundary with simplified GeoJSON","tags":["Constituency boundaries"],"description":"Single constituency boundary by Open North source_set + slug. The two path params reconstruct constituency_id = source_set + '/' + slug — this keeps slashes out of the URL path. Examples: `/boundaries/federal-electoral-districts-2023-representation-order/35064` (federal ridings use numeric slugs), `/boundaries/alberta-electoral-districts/calgary-bow` (provincial ridings use kebab-case slugs). The returned GeoJSON is the simplified geometry (boundary_simple, ~555m tolerance) — good for web maps but not authoritative for edge cases; for exact membership use /boundaries/lookup. Optional ?precision=6 lowers GeoJSON coordinate precision for bandwidth-constrained clients (default 6). 404 on missing boundary. Response: `{ boundary: { constituency_id, name, level, province_territory, source_set, area_sqkm, centroid, effective_from, effective_to, boundaries_version, boundary_geojson } }`. Cache-Control: public, max-age=3600.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":15},"in":"query","name":"precision","required":false,"description":"ST_AsGeoJSON precision (decimal places of lat/lng). Default 6."},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[a-z0-9-]+$"},"in":"path","name":"source_set","required":true,"description":"Open North boundary set, e.g. federal-electoral-districts-2023-representation-order"},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[a-z0-9-]+$"},"in":"path","name":"slug","required":true,"description":"Per-set boundary slug (kebab-case for provincial, numeric for federal)"}],"responses":{"200":{"description":"Default Response"}}}},"/politicians":{"get":{"summary":"Paginated politicians list with civic-app filters","tags":["Politicians (list)"],"description":"Paginated list of politicians for seeding contact-card UIs. Defaults to status='sitting' so the typical 'find me the 87 current AB MLAs' use case is one call: ?jurisdiction=AB&role=mla. Filters: jurisdiction (federal | 2-letter province code), role (substring match against elected_office), status (sitting | former | all; default sitting), constituency_id (exact match), q (name substring; trigram-indexed). Response shape: `{ items: [{ id, full_name, honorific, party, status, level, province_territory, constituency_id, constituency_name, elected_office, photo_url, email, phone, term_start_at, last_verified_at }, ...], page, limit, total, pages }`. Cache-Control: public, max-age=300.","parameters":[{"schema":{"type":"string","minLength":2,"maxLength":8},"in":"query","name":"jurisdiction","required":false,"description":"'federal' maps to level=federal; 2-letter province code (AB, BC, ...) maps to level=provincial AND province_territory=code"},{"schema":{"type":"string","minLength":1,"maxLength":60},"in":"query","name":"role","required":false,"description":"Case-insensitive substring match on elected_office (e.g. 'mla', 'mp', 'councillor')"},{"schema":{"type":"string","enum":["sitting","former","all"]},"in":"query","name":"status","required":false,"description":"Defaults to 'sitting'"},{"schema":{"type":"string","minLength":1,"maxLength":200},"in":"query","name":"constituency_id","required":false,"description":"Open North constituency_id (source_set/slug), matched exactly"},{"schema":{"type":"string","minLength":1,"maxLength":200},"in":"query","name":"q","required":false,"description":"Name substring (trigram-indexed)"},{"schema":{"type":"integer","minimum":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/politicians/{id}":{"get":{"summary":"Single politician with contact info, websites + boundary","tags":["Politicians"],"description":"Returns the politician row enriched with contact info (email, phone, fax), formatted office addresses (constituency + legislature), derived honorific, current status ('sitting' | 'former'), most-recent term dates, all currently-active websites with their latest infrastructure scan, and the constituency boundary GeoJSON. 404 on missing or malformed id. Response: `{ politician: {...}, websites: [...], boundary: {...} | null }` — the politician object carries the contact fields. Cache-Control: public, max-age=60. Schema gaps documented in /developers/contact: honorific is best-effort regex-derived from name; status returns 'sitting' | 'former' only (deceased politicians appear as 'former'); mailing_address is always null in v1.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true,"description":"UUID of the politician"}],"responses":{"200":{"description":"Default Response"}}}},"/politicians/{id}/offices":{"get":{"summary":"Structured office list for one politician","tags":["Politician contact"],"description":"Constituency, legislature, and other offices for a single politician. Multiple rows per politician are preserved — a politician with two constituency offices returns both rows (deduplication is by (politician_id, kind, phone)). Includes lat/lng coordinates, hours-of-operation, fax, and source attribution. 404 on unknown politician. Response: `{ items: [{ kind, address, city, province_territory, postal_code, phone, fax, email, hours, lat, lng, source }, ...] }`. Cache-Control: public, max-age=600.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f-]{36}$"},"in":"path","name":"id","required":true,"description":"UUID of the politician"}],"responses":{"200":{"description":"Default Response"}}}},"/postcodes/{postcode}":{"get":{"summary":"Resolve postcode/FSA to lat/lng + containing ridings","tags":["Postcodes"],"description":"Real-time proxy to Open North's Represent API for postcode geocoding, then runs our own point-in-polygon against the centroid so the `boundaries.*` slots are byte-identical to what /boundaries/lookup returns. Accepts either a 6-char postcode (K1A0A6, K1A 0A6, K1A-0A6) or a 3-char FSA (K1A). Response: `{ postcode, is_fsa, latlng: { lat, lng }, city, province, source: 'nar'|'cache'|'fsa_derived', fetched_at, address_points, approximate, boundaries: { federal: {...}|null, provincial: {...}|null, municipal: {...}|null } }`. Resolution is fully local as of 2026-08-18 — no upstream call. `source` is `nar` for an exact match in the Statistics Canada National Address Register (855,905 codes), `cache` for a code the register cannot supply (PO-box-only, rural-route-only and government codes such as K1A 0A6), and `fsa_derived` when the answer was averaged from other codes in the same forward sortation area. ⚠ `approximate: true` accompanies every `fsa_derived` answer and must not be presented as an exact location: 33-71% of FSAs cross a federal riding boundary, and FSA-level resolution picks the correct federal district only ~68% of the time for rural codes (against 98% for an exact match). `address_points` is how many civic addresses were averaged; 1 is positionally weaker than a high count. Returns 400 on malformed input and 404 when neither the register, the override table, nor the FSA has any location on file. No 503 path remains. Cache-Control: public, max-age=86400.","parameters":[{"schema":{"type":"string","minLength":3,"maxLength":8},"in":"path","name":"postcode","required":true,"description":"6-char postcode (K1A0A6) or 3-char FSA (K1A)"}],"responses":{"200":{"description":"Default Response"}}}},"/postcodes/{postcode}/representatives":{"get":{"summary":"Resolve postcode/FSA to all sitting representatives at every level","tags":["Postcodes"],"description":"Composite endpoint that returns the same `{ postcode, latlng, source, fetched_at, boundaries }` shape as /postcodes/:postcode plus a `representatives` array — one entry per sitting politician whose constituency contains the centroid, including their offices and live social handles. Returns mayor + the specific ward councillor whose boundary contains the postcode at the municipal level (no city-wide councillor fanout). Sitting derivation: politicians.is_active = true AND a politician_terms row with ended_at IS NULL exists. Social handles filtered to is_live=true. Accepts the same 6-char or 3-char-FSA input as /postcodes/:postcode. Cache-Control: public, max-age=300.","parameters":[{"schema":{"type":"string","minLength":3,"maxLength":8},"in":"path","name":"postcode","required":true,"description":"6-char postcode (K1A0A6) or 3-char FSA (K1A)"}],"responses":{"200":{"description":"Default Response"}}}},"/coverage":{"get":{"summary":"Coverage rollup across all 14 Canadian jurisdictions","tags":["Coverage"],"description":"Returns one row per jurisdiction (federal + 10 provinces + 3 territories) with current ingestion status and counts. Includes a small summary rollup for headline numbers. Response shape: `{ jurisdictions: [{ jurisdiction, legislature_name, seats, bills_status, hansard_status, votes_status, committees_status, bills_count, speeches_count, votes_count, politicians_count, last_verified_at, ... }], summary: { total, live, partial, blocked, none } }`. Cache-Control: public, max-age=300.","parameters":[{"schema":{"type":"string","enum":["live","partial","blocked","none"]},"in":"query","name":"status","required":false,"description":"Filter to jurisdictions whose bills_status matches"}],"responses":{"200":{"description":"Default Response"}}}},"/jurisdiction-sources":{"get":{"summary":"Flat per-jurisdiction list (no summary rollup)","tags":["Coverage"],"description":"Same underlying data as /coverage, but as a raw list of rows without the summary block. For callers building their own dashboard view. Response shape: `{ items: [...] }` where each item is the same row shape as /coverage. Cache-Control: public, max-age=300.","responses":{"200":{"description":"Default Response"}}}}},"servers":[{"url":"https://canadianpoliticaldata.org/api/public/v1","description":"Production"}]}