Customer-facing JSON API for voters, donations, volunteers, canvassing, turfs, lawn signs, events, tags, engagement paths, segments, contact requests (cases), surveys, press contacts, membership, pledges, goals, nominations, analytics and campaign settings. All keys are scoped per campaign.
ced_). It lives at /docs/api/electoral under the path /api/v1/electoral/*. The Customer Data API documented here (paths /api/v1/voters, /donations, etc., keys rd_live_) is the paid CRM API for active campaigns. See also the APIs hub for a side-by-side comparison.The OpenAPI spec at /openapi/v1.yaml can be fed directly into openapi-generator-cli (or any other OpenAPI tool) to produce a typed client in your language of choice. Examples:
npx @openapitools/openapi-generator-cli generate \ -i https://ridingdesk.ca/openapi/v1.yaml \ -g typescript-fetch \ -o ./ridingdesk-client
openapi-generator-cli generate \ -i https://ridingdesk.ca/openapi/v1.yaml \ -g python \ -o ./ridingdesk-client
openapi-generator-cli generate \ -i https://ridingdesk.ca/openapi/v1.yaml \ -g go \ -o ./ridingdesk-client
RidingDesk hosts a remote Model Context Protocol server that wraps this API as tools, so Claude, ChatGPT (or any MCP client) can read and write your campaign data in plain English. Add this URL as a custom connector (Claude: Settings → Connectors → Add custom connector), sign in and pick the permissions:
https://ridingdesk.ca/mcp
Campaign managers on any plan, the free Community plan included, can connect; each connection is listed, and revocable, under API keys → Connected apps. By default the server lists only the core tools (voters, canvassing, donations, volunteers, events, the campaign and its overview) and Claude reaches the rest with find_tools, run_read_tool and run_write_tool; it never offers a tool the connection’s permissions can’t use. Use https://ridingdesk.ca/mcp?tools=full to list every tool directly.
Pass your key in the Authorization header. Keys are issued from RidingDesk → Settings → API Keys.
curl https://ridingdesk.ca/api/v1/campaign \ -H "Authorization: Bearer rd_live_..."
Keys start with rd_live_. Treat them as secrets. The full key is shown once at creation time and is never retrievable again.
Plans: every plan can mint live keys, the free COMMUNITY plan included; only the rate limit differs (see Rate limits). The public demo campaign can’t mint keys. A campaign holds at most 10 live keys at once (revoke one to make room; creating an 11th returns 409 with code api_key_limit).
Every key explicitly enumerates the scopes it can use. There are no wildcards.
voters:read voters:write voters:delete donations:read donations:write volunteers:read volunteers:write volunteers:delete canvassing:read canvassing:write lawn_signs:read lawn_signs:write lawn_signs:delete campaign:read campaign:write events:read events:write tags:read tags:write tags:delete paths:read paths:write segments:read segments:write segments:delete turfs:read turfs:write turfs:delete organizations:read organizations:write organizations:delete custom_fields:read custom_fields:write custom_fields:delete households:read goals:read goals:write goals:delete pledges:read pledges:write membership:read membership:write cases:read cases:write press:read press:write press:delete surveys:read surveys:write surveys:delete nominations:read nominations:write nominations:delete analytics:read election_day:read messages:read messages:write drips:read drips:write drips:delete peer_texting:read peer_texting:write website:read website:write website:delete landing_pages:read landing_pages:write landing_pages:delete reports:read reports:write reports:delete
The last six areas — messages, drips, peer-to-peer texting, website pages, landing pages and scheduled reports — are drafts only through the API: see Outreach and web drafts.
Write scopes are never added to an existing key: create a new key (or reconnect an app) with the write boxes ticked. Deleting is its own permission — voters:write can’t delete voters,voters:delete can — so tick a Delete box only for integrations you trust to remove records.
Per-tier, per-key, per-minute:
COMMUNITY 60 req/min STARTER 60 req/min PRO 300 req/min CAMPAIGN_HQ 1200 req/min
Every 429 Too Many Requests response includes:
Retry-After: 60 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0
Retry-After is in seconds. Excessive 429s are logged to your audit trail.
{ "data": { ... } }{
"data": [ ... ],
"pagination": { "nextCursor": "eyJpZCI6...", "limit": 50 }
}{ "error": "Human-readable message", "code": "validation_failed" }unauthorized 401 no/bad key
forbidden 403 key lacks the required scope
not_found 404 resource doesn't exist (or isn't yours)
validation_failed 400 body or params failed validation
conflict 409 duplicate name/slug, or the record's state no longer allows it
(drafts-only areas: the item is live)
validation_failed 422 drafts-only areas: a status / schedule / publish field was sent
limit_exceeded 402 per-tier capacity exhausted (e.g. voter cap)
rate_limit_exceeded 429 per-key bucket exhausted
idempotency_key_reused 422 Idempotency-Key reused with a different body
idempotency_request_in_progress
409 the first request with this Idempotency-Key is still running
(retry with the same key after Retry-After seconds)
idempotency_request_abandoned
409 the first request with this Idempotency-Key never finished; it
may or may not have saved. Check, then retry with a new key
internal 500 server error402 Payment Required with limit_exceeded fires when a write would push the campaign past its plan’s per-tier capacity (today: voter count). Surface the message — it tells the user exactly which limit was hit and which tier is needed to lift it.
For POST/PATCH/DELETE, supply an Idempotency-Key header (any unique string — UUID is recommended). If the same key reaches the server a second time with the same body, the original response is replayed — no duplicate writes, with an Idempotency-Replayed: true header. Keys are remembered for 24 hours.
The key is claimed before the write starts, so a retry sent while the first request is still running can’t write twice either: it waits up to about 10 seconds for the first one to finish and gets the same response. If the first is still running after that, the retry gets 409 idempotency_request_in_progress with a Retry-After header; retry with the same key. Server errors (5xx) are not remembered: retrying with the same key runs the request again.
Idempotency-Key: 7b2e9c1c-3f0d-4a8a-9c1b-bc8c4f5b2e1a
Cursor-based. Pass cursor from the previous response'spagination.nextCursor to fetch the next page. Defaultlimit is 50, max 200.
/api/v1/votersscope: voters:readOptional filters: q (free-text search over name, “First Last”, address, email, postal code, and phone digits in any format), riding, support_level (1-5). Cursor pagination via cursor + limit.
{
"data": [
{ "id": "vtr_...", "firstName": "Jane", "lastName": "Smith", "supportLevel": 5, ... }
],
"pagination": { "nextCursor": null, "limit": 50 }
}/api/v1/votersscope: voters:write{
"firstName": "Jane",
"lastName": "Smith",
"address": "123 Elgin St",
"city": "Ottawa",
"province": "ON",
"postalCode": "K2P 1L5",
"riding": "Ottawa Centre"
}Returns 402 limit_exceeded when adding this voter would push the campaign past its tier’s voter cap.
/api/v1/voters/{id}scope: voters:read/api/v1/voters/{id}scope: voters:write/api/v1/voters/{id}scope: voters:deletePermanent, like Delete on the voter page: the voter’s tags, canvass and call history, survey responses and saved-list memberships are deleted with them; lawn signs stay, unlinked.
A voter’s tags are free-text labels — the ones on the voter page, the voter list’s tag filter and segments’ has_tag. They are separate from the tag library under Tags.
/api/v1/voters/{id}/tagsscope: tags:write{ "tag": "yard-sign" }/api/v1/voters/{id}/tags?tag=yard-signscope: tags:write/api/v1/voters/bulk-tagsscope: tags:write{ "action": "add", "tag": "gotv", "voterIds": ["vtr_...", "vtr_..."] }{ "data": { "action": "add", "tag": "gotv", "matched": 2, "changed": 1, "notFound": [] } }/api/v1/donationsscope: donations:readOptional filters: q (donor-name/email/address text search, case-insensitive), receipt_status (PENDING / ISSUED / SPOILED), method (CREDIT_CARD / ETRANSFER / CHEQUE / CASH / OTHER / IN_KIND), date_from and date_to(inclusive YYYY-MM-DD bounds on the donation date).
/api/v1/donationsscope: donations:write{
"donorName": "Jane Smith",
"donorAddress": "123 Elgin St",
"donorCity": "Ottawa",
"donorProvince": "ON",
"donorPostalCode": "K2P 1L5",
"amount": 250,
"date": "2026-05-02",
"method": "ETRANSFER",
"donorIsCanadianAttested": true
}The federal individual contribution cap (Canada Elections Act s. 367) is enforced server-side. Requests that would push the donor over their annual eligible total return 422 validation_failed before any write occurs.
/api/v1/donations/{id}scope: donations:read/api/v1/donations/{id}/receiptscope: donations:writeIssues an ITR 2000-compliant federal political contribution receipt. Six CRA gates apply (federal-only, monetary-only, agent-on-file, writ-window, EDA leader-authorization, donor-citizenship attestation). Failures return 422 validation_failed with an actionable message.
/api/v1/donations/{id}/cancel-etransferscope: donations:writeCancels a pending Interac e-Transfer pledge (settlement status PENDING → CANCELLED), as the Fundraising page’s Cancel does. The donation record is kept. Donations can’t be deleted, and settled or already-cancelled ones return 409 conflict.
/api/v1/volunteersscope: volunteers:readRoster of campaign volunteers, newest-first, paginated.
/api/v1/volunteersscope: volunteers:write{
"name": "Riley Singh",
"email": "riley@example.ca",
"phone": "6135550101",
"role": "Canvassing",
"status": "ACTIVE",
"skills": ["French"]
}Like “Add volunteer”, the email address is recorded as implied consent to campaign email (CASL).
/api/v1/volunteers/{id}scope: volunteers:write/api/v1/volunteers/{id}scope: volunteers:deletePermanent: their shifts are deleted and lawn signs assigned to them become unassigned. To keep the record, PATCH status to INACTIVE instead.
/api/v1/canvassing/interactionsscope: canvassing:readOptional filter: voter_id to restrict to interactions with a single voter.
/api/v1/canvassing/interactionsscope: canvassing:write{
"voterId": "vtr_...",
"type": "DOOR",
"disposition": "STRONG_SUPPORT",
"notes": "Spoke at door, will put up a sign"
}/api/v1/canvassing/walklistscope: canvassing:readReturns at most 200 voters, ordered for door-knocking. Filters narrow byriding, poll (comma-separated codes),support_levels (comma-separated 1-5), andexclude_contacted (boolean — skip voters with a non-nulllastContact). Server-side pagination is intentionally a no-op; fetch the whole page each call.
/api/v1/lawn-signsscope: lawn_signs:readOptional filter: status (REQUESTED / INSTALLED / COLLECTED).
/api/v1/lawn-signsscope: lawn_signs:write/api/v1/lawn-signs/{id}scope: lawn_signs:read/api/v1/lawn-signs/{id}scope: lawn_signs:writeSetting status to INSTALLED or COLLECTED also stamps the matching timestamp server-side.
/api/v1/lawn-signs/{id}scope: lawn_signs:delete/api/v1/mescope: none (any key)Describes the calling key: key (id, name, prefix, kind, expiry), campaign (id, name) and its scopes. Any campaign key may call it. The MCP server uses it to offer only the tools a key can use.
/api/v1/campaignscope: campaign:readReturns metadata for the campaign that owns the API key —candidateName, party, riding,electionLevel, electionDate,subscriptionTier, official-agent fields, theleaderAuthorizationOnFile flag, contribution and spending limits, and the volunteer signup options.
/api/v1/campaignscope: campaign:writeChanges only these settings: candidateName, party, riding, ednNumber, province, municipalOfficeType, wardName, municipalityName, contributionLimit, spendingLimit, fundraisingGoal, slug (the public donation page address; null unpublishes it, 409 if taken) and volunteerRoles (the volunteer signup options, 1–20). Anything else — e-Transfer deposit details, the official agent and receipt-authority fields, billing, team, keys, domains — is dashboard-only and returns 400.
/api/v1/eventsscope: events:read/api/v1/eventsscope: events:write{
"title": "Town hall",
"slug": "town-hall-sept",
"startsAt": "2029-09-01T23:00:00Z",
"location": "Main library",
"capacity": 80
}New events are DRAFT unless you send a status. The slug must be unique in the campaign (409 otherwise).
/api/v1/events/{id}scope: events:writeSet status to PUBLISHED to publish or CANCELLED to cancel; events and their RSVPs are never deleted through the API.
/api/v1/events/{id}/rsvpsscope: events:read/api/v1/events/{id}/rsvps/{rsvpId}/check-inscope: events:writeThe campaign’s tag library (the Tags page). To tag voters, see Voter tags.
/api/v1/tagsscope: tags:read/api/v1/tagsscope: tags:write{ "name": "Sign location", "color": "#16A34A", "description": "Has room for a lawn sign" }/api/v1/tags/{id}scope: tags:delete/api/v1/pathsscope: paths:read/api/v1/paths/{id}scope: paths:read/api/v1/pathsscope: paths:write{
"name": "Volunteer ladder",
"category": "VOLUNTEER",
"stages": [{ "name": "Signed up" }, { "name": "First shift" }, { "name": "Team lead" }]
}/api/v1/paths/{id}/enrollmentsscope: paths:write{ "recipientName": "Ari Cohen", "recipientEmail": "ari@example.ca" }/api/v1/paths/{id}/enrollments/{enrollmentId}/movescope: paths:write{} moves one stage on (from the last stage it completes the enrollment); { "direction": "prev" } one back; { "toStageId": "..." } jumps; { "complete": true } or { "cancel": true } takes the person off the path, keeping the record and its history.
/api/v1/segmentsscope: segments:read/api/v1/segmentsscope: segments:write{
"entityType": "VOTER",
"name": "Strong supporters with email",
"filter": { "all": [
{ "field": "supportLevel", "op": "lte", "value": 2 },
{ "field": "email", "op": "not_null" }
] }
}/api/v1/segments/{id}scope: segments:deleteDeleting a segment removes only the saved filter.
/api/v1/voters/listsscope: voters:read/api/v1/voters/lists/{id}scope: voters:read/api/v1/voters/listsscope: voters:write{ "name": "Poll 12 supporters", "filters": { "poll": "012", "supportLevel": "1,2" } }
// or a fixed set: { "name": "Call back", "voterIds": ["vtr_...", "vtr_..."] }/api/v1/voters/lists/{id}scope: voters:delete/api/v1/voters/facetsscope: voters:readFacets are the values the Voters page filters on (ridings, polls, tags…). A voter’s interaction history and logging a call are GET /api/v1/canvassing/interactions?voter_id= and POST /api/v1/canvassing/interactions with type: "PHONE".
/api/v1/householdsscope: households:read/api/v1/households/rebuild-keysscope: voters:writeRebuilding regroups voters into households by address, like the Households page’s button.
/api/v1/custom-fieldsscope: custom_fields:read/api/v1/custom-fieldsscope: custom_fields:write{ "entityType": "VOTER", "name": "Language", "slug": "language", "type": "SELECT", "options": ["English", "French"] }/api/v1/custom-fields/{id}scope: custom_fields:deleteDeleting a field deletes every value stored in it.
/api/v1/turfsscope: turfs:read/api/v1/turfs/teamscope: turfs:read/api/v1/turfsscope: turfs:write{ "name": "Glebe north", "type": "STREET_RANGES",
"streetRanges": [{ "street": "Bank St", "fromNumber": 600, "toNumber": 800, "side": "BOTH" }] }/api/v1/turfs/{id}scope: turfs:write/api/v1/turfs/{id}scope: turfs:delete/api/v1/turfs/assignmentsscope: turfs:write{ "userId": "usr_...", "turfIds": ["trf_..."] }/api/v1/turfs/assignments?turfId={turfId}&userId={userId}scope: turfs:delete?counts=true on the list adds each turf’s voter count; /turfs/team lists who turfs can be assigned to.
/api/v1/organizationsscope: organizations:read/api/v1/organizationsscope: organizations:write{ "name": "Ottawa Teachers", "type": "UNION", "contactEmail": "info@example.ca" }/api/v1/organizations/{id}scope: organizations:delete/api/v1/goalsscope: goals:read/api/v1/goalsscope: goals:write{ "kind": "DOORS", "target": 5000, "deadline": "2026-10-20" }/api/v1/goals/{id}scope: goals:delete/api/v1/pledgesscope: pledges:read/api/v1/pledgesscope: pledges:write{ "donorName": "Ari Cohen", "amount": 250, "dueBy": "2026-10-15" }/api/v1/pledges/{id}scope: pledges:write{ "status": "FULFILLED" } marks a pending pledge received, { "status": "CANCELLED" } cancels it. Pledges aren’t deleted.
/api/v1/membership/plansscope: membership:read/api/v1/membership/plansscope: membership:write/api/v1/membership/plans/{id}scope: membership:write/api/v1/membership/membersscope: membership:read/api/v1/membership/members/{id}scope: membership:writeA plan update takes { "isActive": false } (hide from the public page) or true. Members join through the public form. A member update takes { "status": "ACTIVE" } (mark a pending signup paid, or reinstate), CANCELLED or LAPSED.
/api/v1/casesscope: cases:read/api/v1/cases/{id}scope: cases:read/api/v1/casesscope: cases:write{ "constituentName": "Maya Roy", "subject": "Passport delay", "description": "...", "priority": "HIGH" }/api/v1/cases/{id}scope: cases:write/api/v1/cases/{id}/updatesscope: cases:writeSetting status to RESOLVED or CLOSED closes the case. Updates take body and an optional isInternal. Cases aren’t deleted.
/api/v1/reportersscope: press:read/api/v1/reportersscope: press:write/api/v1/reporters/{id}scope: press:write/api/v1/reporters/{id}scope: press:delete{ "markPitched": true } records a pitch.
/api/v1/surveysscope: surveys:read/api/v1/surveys/{id}scope: surveys:read/api/v1/surveysscope: surveys:write{ "name": "Doorstep issues", "questions": [
{ "id": "q1", "type": "multiple_choice", "text": "Top issue?", "options": ["Housing", "Transit"] },
{ "id": "q2", "type": "rating", "text": "How likely are you to vote?" }
] }/api/v1/surveys/{id}scope: surveys:write/api/v1/surveys/{id}scope: surveys:deleteNew surveys are drafts; { "status": "ACTIVE" } opens one and CLOSED closes it. Results are aggregates; free-text answers are counted, not returned. Deleting a survey deletes its responses.
/api/v1/nominationsscope: nominations:read/api/v1/nominationsscope: nominations:write/api/v1/nominations/{id}scope: nominations:write/api/v1/nominations/{id}scope: nominations:deleteAdding an endorsement requires "declaration": true, as the form’s checkbox does; PATCH takes isVerified. Signed papers are uploaded in the dashboard.
Messages, drip sequences, peer-to-peer texting, website pages, landing pages and scheduled reports can be written through the API, but only as drafts. Creates start as DRAFT (drip sequences inactive, report schedules paused); a status, scheduledAt, publishedAt, enabled or isActive field is refused with 422; anything live — sent, sending, scheduled, in review, active, published, archived or switched on — can’t be edited or deleted (409 conflict). Sending, scheduling, activating and publishing are done in the dashboard.
/api/v1/messagesscope: messages:read/api/v1/messages/{id}scope: messages:read/api/v1/messagesscope: messages:write{ "name": "GOTV reminder", "subject": "Polls open at 9",
"body": "Hi {{firstName}}, polls open at 9.",
"recipientFilter": { "source": "VOTERS", "supportLevel": 1 } }/api/v1/messages/{id}scope: messages:write/api/v1/messages/{id}/ab-variantsscope: messages:write/api/v1/messages/recipient-countscope: messages:read/api/v1/broadcastersscope: messages:read/api/v1/sms-keywordsscope: messages:readtype is EMAIL (default; needs a subject), SMS or PHONE. The recipient count returns numbers only. Sender identities and SMS keywords are read-only; copy a sender’s fromName / fromEmail into a draft.
/api/v1/dripsscope: drips:read/api/v1/drips/{id}scope: drips:read/api/v1/dripsscope: drips:write/api/v1/drips/{id}/stepsscope: drips:write/api/v1/drips/{id}/steps/{stepId}scope: drips:write/api/v1/drips/{id}/steps/{stepId}scope: drips:deleteSequences are created inactive. Steps can be changed only while the sequence is inactive (never activated, or paused); while it is active, step writes return 409.
/api/v1/p2pscope: peer_texting:read/api/v1/p2p/{id}scope: peer_texting:read/api/v1/p2pscope: peer_texting:writeCreates a draft with up to 2,000 contacts. Volunteers can’t claim batches until it’s activated in the dashboard. Reads return counts, not the contact list.
/api/v1/website/pagesscope: website:read/api/v1/website/pages/{id}scope: website:read/api/v1/website/pagesscope: website:write/api/v1/website/pages/{id}scope: website:write/api/v1/website/pages/{id}scope: website:delete/api/v1/website/pages/{id}/translations/{locale}scope: website:write/api/v1/website/pages/{id}/translations/{locale}scope: website:deletelocale is en or fr. A draft that was published before can be edited but not deleted. Images and the theme are set in the dashboard.
/api/v1/landing-pagesscope: landing_pages:read/api/v1/landing-pages/{id}scope: landing_pages:read/api/v1/landing-pagesscope: landing_pages:write/api/v1/landing-pages/{id}scope: landing_pages:write/api/v1/landing-pages/{id}scope: landing_pages:delete/api/v1/reports/scheduledscope: reports:read/api/v1/reports/scheduledscope: reports:write/api/v1/reports/scheduled/{id}scope: reports:write/api/v1/reports/scheduled/{id}scope: reports:deleteSchedules are created paused and email no one until they’re switched on in the dashboard; only paused schedules can be edited or deleted.
/api/v1/donations/statsscope: donations:read/api/v1/donations/compliancescope: donations:read/api/v1/donors/call-listscope: donations:read/api/v1/donors/call-attemptsscope: donations:write{ "donorName": "Jane Smith", "outcome": "PLEDGED", "pledgedAmount": 500 }/api/v1/analytics/overviewscope: analytics:read/api/v1/analytics/growthscope: analytics:read/api/v1/analytics/funnelscope: analytics:read/api/v1/analytics/leaderboardscope: analytics:read/api/v1/analytics/recruitersscope: analytics:readGrowth takes period=week|month, funnel days, the leaderboard window=week|month|all.
/api/v1/election-day/gotvscope: election_day:read/api/v1/election-day/pollsscope: election_day:read/api/v1/election-day/votedscope: voters:writeThe GOTV list is supporters who haven’t voted yet, with phone and address — it is voter data, so grant election_day:read with care. Polls gives supporter turnout per poll (supporters, how many are marked as voted, and the percentage) plus the totals.
Mark voters as voted with { "voterIds": ["…"], "voted": true } (up to 500 ids; one is fine). Send "voted": false to clear it. Marked voters leave the GOTV list and get no more GOTV reminders. The answer has changed, unchanged (already in that state; an earlier mark keeps its time) and notFound (ids that aren’t your voters), so a retry is harmless.
Revoke immediately from RidingDesk → Settings → API Keys. Your audit log shows every action the key took between creation and revocation — review for unauthorized donations or voter changes and reverse them by hand.