API Reference

Overview

RosaVote is API-first: the admin console is a thin client over these endpoints, so anything it does can be scripted. Base URL is this deployment's origin. All responses are JSON unless noted (ballot/console pages return HTML; BLT and ZIP exports return files).

Authentication

Admin endpoints take an admin token in the X-Admin-Token header. Tokens are stored only as SHA-256 hashes. National tokens can do everything; chapter tokens are scoped to their own polls and cannot build elections, import rolls, set weights, or run lookups. Voter endpoints take a one-time voting code in the request body, never a header. Every administrative action is written to an append-only audit log under the token's name.

Conventions

<poll> is a poll id ([a-z0-9_]{3,64}). Errors are {error, message} with a 4xx/5xx status. Rate note: the service scales on Cloud Run behind an instance cap — floods become slowness, not failures, and never affect an in-progress vote.

Public — voter

MethodPathAuthWhat it does
GET/p/<poll>/NoneThe branded ballot page for a poll.
POST/p/<poll>/resendNoneSelf-serve 'lost my code'. Body {contact}. Enumeration-safe (always {status:ok}), rate-limited 15 min/contact; re-sends the member's link to their on-record contacts only.
GET/prefsNoneSelf-serve notification preferences page (opt a contact out / back in).
POST/prefs/optoutNoneSuppress election contact to an email/phone. Body {contact}. Enumeration-safe.
POST/prefs/optinNoneRe-enable election contact to an email/phone. Body {contact}.
GET/p/<poll>/v/<code>NoneBallot page with a one-tap voting code embedded.
GET/p/<poll>/voted?code=…NoneHas this code already voted? → {voted: bool}. Malformed/unknown codes are rejected before any DB read.
POST/p/<poll>/voteVoting code (in body)Cast a ballot: {code, answers:{questionKey:value}}. One code = one vote, enforced atomically. → {status:'recorded', receipt}.
POST/p/<poll>/provisionalNoneCast a sealed provisional ballot with identifying info for later adjudication.
GET/p/<poll>/verify?receipt=…NoneConfirm a ballot was stored → {found, status}. No identity, no answers — safe to share.
GET/p/<poll>/resultsNonePublic results page (only after finalize + publish). Aggregate only; served from a frozen cache.

Admin — auth

MethodPathAuthWhat it does
GET/admin/api/whoamiX-Admin-TokenResolve the caller's identity → {name, role, polls}.
POST/admin/api/adminsNationalMint a scoped admin token (plaintext returned once). Body: {name, role, polls, token?}.

Admin — elections

MethodPathAuthWhat it does
GET/admin/api/pollsAny adminList polls visible to this token (chapter tokens see only their own).
POST/admin/api/polls/<poll>NationalCreate or update a poll config (the whole ballot schema). unfinalize:true to reopen a certified poll (audited).
POST/admin/api/polls/<poll>/openPoll adminOpen voting now (with or without a schedule).
POST/admin/api/polls/<poll>/closePoll adminClose voting now.
POST/admin/api/polls/<poll>/archiveNationalArchive a poll: hide it from voting/public and the normal list (ballots kept, reversible).
POST/admin/api/polls/<poll>/unarchiveNationalRestore an archived poll to the active set.
POST/admin/api/polls/<poll>/publishPoll adminPublish/unpublish the public results page (finalized polls). Body: {publish:bool}.
POST/admin/api/cron/closeoutNationalFinalize every poll past its window. Called by Cloud Scheduler every 15 min; idempotent.

Admin — results & tabulation

MethodPathAuthWhat it does
GET/admin/api/polls/<poll>/resultsPoll adminFull tallies (finalized, or national + ?live=1, audited). ?fresh=1 recomputes past the cache.
POST/admin/api/polls/<poll>/recount_previewPoll adminPreview a ranked contest under another method (plurality|approval|borda|irv|mntv|meek), or a score contest as score|star|star_pr. Body: {question, method}.
GET/admin/api/polls/<poll>/blt/<qkey>Poll adminDownload a contest as a standard .blt. ?recount=1 alternates; ?withdraw=IDS dropout recount.
GET/admin/api/polls/<poll>/export.zipPoll adminResults package: .txt/.csv/.json reports, printable .html, every .blt, ballots.csv, verify readme. ?anonymize=1.
POST/admin/api/count_bltAny adminRun Scottish/Meek STV on a posted .blt (with optional constraints). Touches no stored data.

Admin — voters

MethodPathAuthWhat it does
GET/admin/api/polls/<poll>/votersPoll adminPer-voter turnout + received-verification + integrity counter. Never how anyone voted. ?member_id=… to search.
POST/admin/api/polls/<poll>/voters/importNationalImport a roll (JSON members[]). Mints hashed codes, returns the plaintext manifest once.
POST/admin/api/polls/<poll>/voters/import_gcsNationalImport a roll CSV from gs://bucket/file.csv (header member_id; optional chapter, weight).
POST/admin/api/polls/<poll>/voters/import_bigqueryNationalImport eligible members by roll chapter straight from the warehouse.
POST/admin/api/polls/<poll>/voters/weightNationalSet a voter's ballot weight (1–1000). Resolved at tally time.

Admin — adjudication & remedy

MethodPathAuthWhat it does
GET/admin/api/polls/<poll>/provisionalsPoll adminPending provisionals — identity fields only; answers stay sealed.
POST/admin/api/polls/<poll>/provisionals/<receipt>Poll adminAdjudicate: {action:'verify', member_id} promotes + burns codes; {action:'reject', note}.
GET/admin/api/polls/<poll>/lookup?member_id|receipt=…NationalTroubleshooting: identity-linked answers; secret questions show recorded-or-not, never content. Audited.
POST/p/<poll>/admin/voidPoll adminVoid a cast ballot (flag, never delete) and reissue a fresh code. Open-window only, audited.

Example

curl -s -H "X-Admin-Token: $TOKEN" \
  https://<origin>/admin/api/polls/my_poll/results?live=1

← Back to RosaVote

RosaVote is free/libre software under AGPL-3.0 · source code · © 2026 Walker Green