Politician socials¶
/api/public/v1/politicians/{id}/socials and
/api/public/v1/politicians/{id}/posts expose the social-media layer:
handles tied to each politician across nine platforms (Twitter, Bluesky,
Mastodon, Instagram, Facebook, YouTube, TikTok, LinkedIn, Threads), plus
the post archive captured by the paid scrape-monitoring pipeline.
Free-tier — any authenticated key works; rate-limited by
tier.
For the full per-endpoint schema with "Try it out" buttons see the Swagger UI under the Politician socials tag.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
GET |
/politicians/{id}/socials |
Live handles for one politician (opt into dead handles with ?include_dead=true) |
GET |
/politicians/{id}/posts |
Scraped posts from the paid monitoring pipeline, with attribution |
GET /politicians/{id}/socials¶
Lists the social-media handles tied to a politician. Filters to
is_live=true by default — dead/abandoned handles drop out of the
response unless you opt back in with ?include_dead=true.
Path parameter¶
| Name | Type | Notes |
|---|---|---|
id |
UUID | The politician's id. |
Query parameters¶
| Name | Type | Notes |
|---|---|---|
include_dead |
bool | If true, includes handles where is_live=false. Default false. |
Response¶
{
"items": [
{
"platform": "twitter",
"handle": "PierrePoilievre",
"url": "https://twitter.com/PierrePoilievre",
"follower_count": 1290000,
"lifetime_post_count": 23410,
"last_post_at": "2026-05-23T14:08:22Z",
"last_profile_check_at": "2026-05-24T07:30:38Z",
"last_verified_at": "2026-05-24T07:30:38Z",
"is_live": true
},
{
"platform": "bluesky",
"handle": "pierrepoilievre.bsky.social",
"url": "https://bsky.app/profile/pierrepoilievre.bsky.social",
"follower_count": 4200,
"lifetime_post_count": null,
"last_post_at": null,
"last_profile_check_at": null,
"last_verified_at": "2026-05-24T07:30:38Z",
"is_live": true
}
]
}
Returns 404 { code: "not_found" } for unknown politician UUIDs.
Cache-Control: public, max-age=300.
Example¶
PID="..." # the politician's UUID — fetch from /politicians/{id} or your own catalog
curl -s -H 'Authorization: Bearer cpd_live_…' \
"https://canadianpoliticaldata.org/api/public/v1/politicians/$PID/socials" \
| jq '.items[] | {platform, handle, follower_count}'
GET /politicians/{id}/posts¶
Returns public-record social posts captured by the paid monitoring pipeline. Visible to everyone — not gated to subscribers — matching the public-record framing in the disclaimer.
Query parameters¶
| Name | Type | Notes |
|---|---|---|
platform |
comma-separated string | Filter to specific platforms, e.g. twitter,bluesky. Allowed: twitter, bluesky, mastodon, instagram, facebook, youtube, tiktok, linkedin, threads. |
limit |
int 1–200 | Default 50. |
Response¶
{
"items": [
{
"id": "uuid",
"politician_id": "uuid",
"platform": "twitter",
"post_id": "1791234567890123456",
"posted_at": "2026-05-23T14:08:22Z",
"text": "Today in Ottawa, we…",
"url": "https://twitter.com/PierrePoilievre/status/1791234567890123456",
"media_urls": ["https://pbs.twimg.com/…"],
"engagement": { "likes": 1240, "reposts": 320, "replies": 88 },
"scraped_at": "2026-05-23T18:15:00Z",
"funded_by": "@civic_observer",
"funded_by_url": "https://civicobserver.ca/"
}
]
}
funded_by and funded_by_url are populated when a paid subscriber
who captured this post opted in to public attribution on their
subscription. Both fields are null for anonymous-funded posts and
for posts captured by admin one-shot runs that weren't linked to an
attribution. Render the attribution as a rel="nofollow noopener
external" link when both fields are set.
Ordering: posted_at DESC, NULLs last. When multiple subscriptions
captured the same post, the most-recently-finished subscription's
attribution wins.
Cache-Control: public, max-age=60.
Example¶
PID="..."
curl -s -H 'Authorization: Bearer cpd_live_…' \
"https://canadianpoliticaldata.org/api/public/v1/politicians/$PID/posts?platform=twitter&limit=10" \
| jq '.items[] | "[\(.posted_at)] \(.text[:120])"'
Caveats¶
follower_countandlifetime_post_countmay be NULL for handles that haven't been profile-checked yet. The monitoring pipeline backfills these lazily; freshly-imported handles arrive withlast_profile_check_at = nulland counts unset.- Handle history is opt-in. Pass
?include_dead=trueto see handles a politician moved away from. Useful for archival research; dropped from the default response to keep the typical UI use case (current-handle linking) clean. - Posts coverage depends on monitoring subscriptions. A politician
with no active subscription will return an empty
itemsarray. Subscribe at/account/monitoringto add monitoring; existing captured posts surface immediately for anyone (including anonymous web visitors) once monitoring is in place. - Engagement shape varies by platform. Twitter posts carry
likes/reposts/replies; Blueskylikes/reposts/replies; Mastodonfavourites/reblogs/replies; Instagramlikes/comments. Treatengagementas an opaque platform-specific object — keys and presence vary.