RidingDesk Customer Data API v1

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.

Looking for the public Electoral Data API? That’s a different product — the free dataset of Canadian electoral boundaries, postal-code lookups, and election results (keys start with 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.
Machine-readable spec available:Download OpenAPI 3.1 spec (YAML)Covers every endpoint, auth, pagination, errors, idempotency, rate limits.

Use the spec with codegen

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:

TypeScript / JavaScript
npx @openapitools/openapi-generator-cli generate \
  -i https://ridingdesk.ca/openapi/v1.yaml \
  -g typescript-fetch \
  -o ./ridingdesk-client
Python
openapi-generator-cli generate \
  -i https://ridingdesk.ca/openapi/v1.yaml \
  -g python \
  -o ./ridingdesk-client
Go
openapi-generator-cli generate \
  -i https://ridingdesk.ca/openapi/v1.yaml \
  -g go \
  -o ./ridingdesk-client

Use it from Claude or ChatGPT (MCP)

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:

Connector URL
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.

Authentication

Pass your key in the Authorization header. Keys are issued from RidingDesk → Settings → API Keys.

curl
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).

Scopes

Every key explicitly enumerates the scopes it can use. There are no wildcards.

scope vocabulary
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.

Rate limits

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.

Response envelope

success — single resource
{ "data": { ... } }
success — list
{
  "data": [ ... ],
  "pagination": { "nextCursor": "eyJpZCI6...", "limit": 50 }
}
error
{ "error": "Human-readable message", "code": "validation_failed" }

Error codes

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 error

402 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.

Idempotency

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

Pagination

Cursor-based. Pass cursor from the previous response'spagination.nextCursor to fetch the next page. Defaultlimit is 50, max 200.

Voters

GET/api/v1/votersscope: voters:read

Optional 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.

response
{
  "data": [
    { "id": "vtr_...", "firstName": "Jane", "lastName": "Smith", "supportLevel": 5, ... }
  ],
  "pagination": { "nextCursor": null, "limit": 50 }
}
POST/api/v1/votersscope: voters:write
request body
{
  "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.

GET/api/v1/voters/{id}scope: voters:read
PATCH/api/v1/voters/{id}scope: voters:write
DELETE/api/v1/voters/{id}scope: voters:delete

Permanent, 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.

Voter tags

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.

POST/api/v1/voters/{id}/tagsscope: tags:write
request body
{ "tag": "yard-sign" }
DELETE/api/v1/voters/{id}/tags?tag=yard-signscope: tags:write
POST/api/v1/voters/bulk-tagsscope: tags:write
request body (max 500 voterIds)
{ "action": "add", "tag": "gotv", "voterIds": ["vtr_...", "vtr_..."] }
response
{ "data": { "action": "add", "tag": "gotv", "matched": 2, "changed": 1, "notFound": [] } }

Donations

GET/api/v1/donationsscope: donations:read

Optional 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).

POST/api/v1/donationsscope: donations:write
request body
{
  "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.

GET/api/v1/donations/{id}scope: donations:read
POST/api/v1/donations/{id}/receiptscope: donations:write

Issues 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.

POST/api/v1/donations/{id}/cancel-etransferscope: donations:write

Cancels 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.

Volunteers

GET/api/v1/volunteersscope: volunteers:read

Roster of campaign volunteers, newest-first, paginated.

POST/api/v1/volunteersscope: volunteers:write
request body
{
  "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).

PATCH/api/v1/volunteers/{id}scope: volunteers:write
DELETE/api/v1/volunteers/{id}scope: volunteers:delete

Permanent: their shifts are deleted and lawn signs assigned to them become unassigned. To keep the record, PATCH status to INACTIVE instead.

Canvassing

GET/api/v1/canvassing/interactionsscope: canvassing:read

Optional filter: voter_id to restrict to interactions with a single voter.

POST/api/v1/canvassing/interactionsscope: canvassing:write
request body
{
  "voterId": "vtr_...",
  "type": "DOOR",
  "disposition": "STRONG_SUPPORT",
  "notes": "Spoke at door, will put up a sign"
}
GET/api/v1/canvassing/walklistscope: canvassing:read

Returns 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.

Lawn signs

GET/api/v1/lawn-signsscope: lawn_signs:read

Optional filter: status (REQUESTED / INSTALLED / COLLECTED).

POST/api/v1/lawn-signsscope: lawn_signs:write
GET/api/v1/lawn-signs/{id}scope: lawn_signs:read
PATCH/api/v1/lawn-signs/{id}scope: lawn_signs:write

Setting status to INSTALLED or COLLECTED also stamps the matching timestamp server-side.

DELETE/api/v1/lawn-signs/{id}scope: lawn_signs:delete

Campaign

GET/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.

GET/api/v1/campaignscope: campaign:read

Returns 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.

PATCH/api/v1/campaignscope: campaign:write

Changes 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.

Events

GET/api/v1/eventsscope: events:read
POST/api/v1/eventsscope: events:write
request body
{
  "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).

PATCH/api/v1/events/{id}scope: events:write

Set status to PUBLISHED to publish or CANCELLED to cancel; events and their RSVPs are never deleted through the API.

GET/api/v1/events/{id}/rsvpsscope: events:read
POST/api/v1/events/{id}/rsvps/{rsvpId}/check-inscope: events:write

Tags

The campaign’s tag library (the Tags page). To tag voters, see Voter tags.

GET/api/v1/tagsscope: tags:read
POST/api/v1/tagsscope: tags:write
request body
{ "name": "Sign location", "color": "#16A34A", "description": "Has room for a lawn sign" }
DELETE/api/v1/tags/{id}scope: tags:delete

Engagement paths

GET/api/v1/pathsscope: paths:read
GET/api/v1/paths/{id}scope: paths:read
POST/api/v1/pathsscope: paths:write
request body
{
  "name": "Volunteer ladder",
  "category": "VOLUNTEER",
  "stages": [{ "name": "Signed up" }, { "name": "First shift" }, { "name": "Team lead" }]
}
POST/api/v1/paths/{id}/enrollmentsscope: paths:write
request body
{ "recipientName": "Ari Cohen", "recipientEmail": "ari@example.ca" }
POST/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.

Segments

GET/api/v1/segmentsscope: segments:read
POST/api/v1/segmentsscope: segments:write
request body
{
  "entityType": "VOTER",
  "name": "Strong supporters with email",
  "filter": { "all": [
    { "field": "supportLevel", "op": "lte", "value": 2 },
    { "field": "email", "op": "not_null" }
  ] }
}
DELETE/api/v1/segments/{id}scope: segments:delete

Deleting a segment removes only the saved filter.

Saved voter lists and facets

GET/api/v1/voters/listsscope: voters:read
GET/api/v1/voters/lists/{id}scope: voters:read
POST/api/v1/voters/listsscope: voters:write
request body
{ "name": "Poll 12 supporters", "filters": { "poll": "012", "supportLevel": "1,2" } }
// or a fixed set: { "name": "Call back", "voterIds": ["vtr_...", "vtr_..."] }
DELETE/api/v1/voters/lists/{id}scope: voters:delete
GET/api/v1/voters/facetsscope: voters:read

Facets 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".

Households

GET/api/v1/householdsscope: households:read
POST/api/v1/households/rebuild-keysscope: voters:write

Rebuilding regroups voters into households by address, like the Households page’s button.

Custom fields

GET/api/v1/custom-fieldsscope: custom_fields:read
POST/api/v1/custom-fieldsscope: custom_fields:write
request body
{ "entityType": "VOTER", "name": "Language", "slug": "language", "type": "SELECT", "options": ["English", "French"] }
DELETE/api/v1/custom-fields/{id}scope: custom_fields:delete

Deleting a field deletes every value stored in it.

Turfs

GET/api/v1/turfsscope: turfs:read
GET/api/v1/turfs/teamscope: turfs:read
POST/api/v1/turfsscope: turfs:write
request body
{ "name": "Glebe north", "type": "STREET_RANGES",
  "streetRanges": [{ "street": "Bank St", "fromNumber": 600, "toNumber": 800, "side": "BOTH" }] }
PATCH/api/v1/turfs/{id}scope: turfs:write
DELETE/api/v1/turfs/{id}scope: turfs:delete
POST/api/v1/turfs/assignmentsscope: turfs:write
request body
{ "userId": "usr_...", "turfIds": ["trf_..."] }
DELETE/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.

Organizations

GET/api/v1/organizationsscope: organizations:read
POST/api/v1/organizationsscope: organizations:write
request body
{ "name": "Ottawa Teachers", "type": "UNION", "contactEmail": "info@example.ca" }
DELETE/api/v1/organizations/{id}scope: organizations:delete

Goals

GET/api/v1/goalsscope: goals:read
POST/api/v1/goalsscope: goals:write
request body
{ "kind": "DOORS", "target": 5000, "deadline": "2026-10-20" }
DELETE/api/v1/goals/{id}scope: goals:delete

Pledges

GET/api/v1/pledgesscope: pledges:read
POST/api/v1/pledgesscope: pledges:write
request body
{ "donorName": "Ari Cohen", "amount": 250, "dueBy": "2026-10-15" }
PATCH/api/v1/pledges/{id}scope: pledges:write

{ "status": "FULFILLED" } marks a pending pledge received, { "status": "CANCELLED" } cancels it. Pledges aren’t deleted.

Membership

GET/api/v1/membership/plansscope: membership:read
POST/api/v1/membership/plansscope: membership:write
PATCH/api/v1/membership/plans/{id}scope: membership:write
GET/api/v1/membership/membersscope: membership:read
PATCH/api/v1/membership/members/{id}scope: membership:write

A 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.

Contact requests (cases)

GET/api/v1/casesscope: cases:read
GET/api/v1/cases/{id}scope: cases:read
POST/api/v1/casesscope: cases:write
request body
{ "constituentName": "Maya Roy", "subject": "Passport delay", "description": "...", "priority": "HIGH" }
PATCH/api/v1/cases/{id}scope: cases:write
POST/api/v1/cases/{id}/updatesscope: cases:write

Setting status to RESOLVED or CLOSED closes the case. Updates take body and an optional isInternal. Cases aren’t deleted.

Press contacts

GET/api/v1/reportersscope: press:read
POST/api/v1/reportersscope: press:write
PATCH/api/v1/reporters/{id}scope: press:write
DELETE/api/v1/reporters/{id}scope: press:delete

{ "markPitched": true } records a pitch.

Surveys

GET/api/v1/surveysscope: surveys:read
GET/api/v1/surveys/{id}scope: surveys:read
POST/api/v1/surveysscope: surveys:write
request body
{ "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?" }
] }
PATCH/api/v1/surveys/{id}scope: surveys:write
DELETE/api/v1/surveys/{id}scope: surveys:delete

New 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.

Nominations

GET/api/v1/nominationsscope: nominations:read
POST/api/v1/nominationsscope: nominations:write
PATCH/api/v1/nominations/{id}scope: nominations:write
DELETE/api/v1/nominations/{id}scope: nominations:delete

Adding an endorsement requires "declaration": true, as the form’s checkbox does; PATCH takes isVerified. Signed papers are uploaded in the dashboard.

Outreach and web drafts

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.

Email & SMS messages

GET/api/v1/messagesscope: messages:read
GET/api/v1/messages/{id}scope: messages:read
POST/api/v1/messagesscope: messages:write
request body
{ "name": "GOTV reminder", "subject": "Polls open at 9",
  "body": "Hi {{firstName}}, polls open at 9.",
  "recipientFilter": { "source": "VOTERS", "supportLevel": 1 } }
PATCH/api/v1/messages/{id}scope: messages:write
PUT/api/v1/messages/{id}/ab-variantsscope: messages:write
POST/api/v1/messages/recipient-countscope: messages:read
GET/api/v1/broadcastersscope: messages:read
GET/api/v1/sms-keywordsscope: messages:read

type 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.

Drip sequences

GET/api/v1/dripsscope: drips:read
GET/api/v1/drips/{id}scope: drips:read
POST/api/v1/dripsscope: drips:write
POST/api/v1/drips/{id}/stepsscope: drips:write
PATCH/api/v1/drips/{id}/steps/{stepId}scope: drips:write
DELETE/api/v1/drips/{id}/steps/{stepId}scope: drips:delete

Sequences 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.

Peer-to-peer texting

GET/api/v1/p2pscope: peer_texting:read
GET/api/v1/p2p/{id}scope: peer_texting:read
POST/api/v1/p2pscope: peer_texting:write

Creates 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.

Website pages

GET/api/v1/website/pagesscope: website:read
GET/api/v1/website/pages/{id}scope: website:read
POST/api/v1/website/pagesscope: website:write
PATCH/api/v1/website/pages/{id}scope: website:write
DELETE/api/v1/website/pages/{id}scope: website:delete
PUT/api/v1/website/pages/{id}/translations/{locale}scope: website:write
DELETE/api/v1/website/pages/{id}/translations/{locale}scope: website:delete

locale 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.

Landing pages

GET/api/v1/landing-pagesscope: landing_pages:read
GET/api/v1/landing-pages/{id}scope: landing_pages:read
POST/api/v1/landing-pagesscope: landing_pages:write
PATCH/api/v1/landing-pages/{id}scope: landing_pages:write
DELETE/api/v1/landing-pages/{id}scope: landing_pages:delete

Scheduled reports

GET/api/v1/reports/scheduledscope: reports:read
POST/api/v1/reports/scheduledscope: reports:write
PATCH/api/v1/reports/scheduled/{id}scope: reports:write
DELETE/api/v1/reports/scheduled/{id}scope: reports:delete

Schedules are created paused and email no one until they’re switched on in the dashboard; only paused schedules can be edited or deleted.

Fundraising and call time

GET/api/v1/donations/statsscope: donations:read
GET/api/v1/donations/compliancescope: donations:read
GET/api/v1/donors/call-listscope: donations:read
POST/api/v1/donors/call-attemptsscope: donations:write
request body
{ "donorName": "Jane Smith", "outcome": "PLEDGED", "pledgedAmount": 500 }

Analytics

GET/api/v1/analytics/overviewscope: analytics:read
GET/api/v1/analytics/growthscope: analytics:read
GET/api/v1/analytics/funnelscope: analytics:read
GET/api/v1/analytics/leaderboardscope: analytics:read
GET/api/v1/analytics/recruitersscope: analytics:read

Growth takes period=week|month, funnel days, the leaderboard window=week|month|all.

Election day

GET/api/v1/election-day/gotvscope: election_day:read
GET/api/v1/election-day/pollsscope: election_day:read
POST/api/v1/election-day/votedscope: voters:write

The 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.

Compromised key?

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.