{"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). For FSAs the centroid is a representative point inside the FSA polygon and the boundary lookup uses that point — good enough for 'what's the dominant riding for this FSA' but not authoritative for FSAs that straddle riding boundaries. Response: `{ postcode, is_fsa, latlng: { lat, lng }, city, province, source: 'cache'|'cache_stale'|'live', fetched_at, boundaries: { federal: {...}|null, provincial: {...}|null, municipal: {...}|null } }`. The `source` field indicates whether this came from the local cache (fresh within 30 days), a stale cache served because Open North was unreachable, or a live Open North fetch. Returns 400 on malformed input, 404 when Open North doesn't know the postcode (the cache row is evicted on confirmed 404), 503 only when Open North is unreachable AND no cache row exists. 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"}]}