# CrownVote Developer API v1 CrownVote Developer API v1 lets developers build custom election frontends while CrownVote handles election scoping, voter authentication, ballot delivery, encrypted vote submission, duplicate prevention, and result integrity. This file is intended for both humans and AI agents and can be exposed as: ```txt /docs/api.md /docs/llms.txt ``` ## Base URL ```txt https://api.crownvote.com/api/v1 ``` ## Core concepts | Term | Meaning | |---|---| | Election | The voting exercise configured on CrownVote. | | Voter | The person authorized to vote in the election. | | Ballot category | A position/category the voter must vote under. | | Candidate | A contestant/nominee/option listed under a ballot category. | | Selection | One submitted encrypted vote value for a ballot category. | | Public key | The election-scoping key sent as `X-PUBLIC-KEY`. | | Voter token | Bearer token returned after voter login. | | Admin token | Token returned after admin login for supervised in-person voting. | --- # Authentication model Every request to CrownVote Developer API must include the election public key: ```http X-PUBLIC-KEY: cv_pub_xxxxx ``` Protected voter requests also require the voter bearer token: ```http Authorization: Bearer oat_xxxxx ``` Supervised in-person requests may also require the admin session token: ```http X-ADMIN-TOKEN: cv_admin_xxxxx ``` ## Headers | Header | Required | Used for | |---|---:|---| | `X-PUBLIC-KEY` | Yes | Scopes every request to the correct election. | | `Content-Type: application/json` | Required for JSON POST requests | Sends request body as JSON. | | `Authorization: Bearer ` | Required after voter login | Authenticates protected voter actions. | | `X-ADMIN-TOKEN` | Required only when `inPersonRequireAdminLogin` is `true` | Authenticates the active admin session for supervised in-person voting. | --- # Standard response format ## Success response ```json { "status": true, "data": {} } ``` ## Error response ```json { "status": false, "code": "ERROR_CODE", "message": "Human readable error message.", "errors": [] } ``` Validation errors may include field-level details: ```json { "status": false, "code": "REQUEST_VALIDATION_FAILED", "message": "Request validation failed.", "errors": [ { "field": "selections", "message": "The selections field must be defined", "rule": "required" } ] } ``` --- # UI construction rules from `GET /election` Every custom frontend should begin by calling `GET /election`. The response determines which UI to construct. ## Main decision tree ```txt GET /election | |-- votingMode = "remote" | | | |-- show remote voter login form | |-- fields: voterId, password | |-- do not show admin login form | |-- do not show voter token form | |-- votingMode = "in_person" | |-- always show voter token form | |-- if inPersonRequireAdminLogin = true | show admin login form first | store admin accessToken in UI/session | send X-ADMIN-TOKEN on supervised requests | |-- if inPersonRequireAdminLogin = false | do not show admin login form | do not send X-ADMIN-TOKEN | |-- if inPersonRequireVoterPassword = true | show voter password field with voter token | |-- if inPersonRequireVoterPassword = false show voter token field only ``` ## Remote voting UI For remote voting, show: ```txt Voter ID input Password input Login button ``` Remote voter login payload: ```json { "voterId": "236165", "password": "voter-password" } ``` ## In-person voting UI For in-person voting, always show: ```txt Voter token input ``` If `inPersonRequireVoterPassword` is `true`, also show: ```txt Voter password input ``` If `inPersonRequireAdminLogin` is `true`, show admin login before allowing voter login: ```txt Admin PIN input Admin password input Admin login button ``` After admin login succeeds, store: ```txt data.admin.accessToken ``` Use it as: ```http X-ADMIN-TOKEN: cv_admin_xxxxx ``` For single-page starter kits, use `sessionStorage` for `adminToken` and `voterToken`. Clear the voter token after every completed vote. Clear the admin token when the polling officer logs out. --- # Election ## Get election Resolve the public election attached to the public key. This endpoint returns the election configuration needed before rendering login forms. ```http GET /api/v1/election HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx ``` ## Response: remote election For remote elections, in-person requirement fields are returned as `null`. ```json { "status": true, "data": { "election": { "title": "SRC General Elections 2026", "institution": "CrownVote Demo Institution", "description": "Official voting event for the SRC General Elections.", "instructions": "Login with your voter ID and password to cast your vote.", "status": "live", "startsAt": "2026-06-01T08:00:00.000Z", "endsAt": "2026-06-01T17:00:00.000Z", "votingMode": "remote", "accessMode": "api", "inPersonRequireAdminLogin": null, "inPersonRequireVoterPassword": null } } } ``` ## Response: in-person election For in-person elections, the response tells the frontend whether admin login and voter password are required. ```json { "status": true, "data": { "election": { "title": "SRC General Elections 2026", "institution": "CrownVote Demo Institution", "description": "Official in-person voting session.", "instructions": "Present your voting token to the polling officer.", "status": "live", "startsAt": "2026-06-01T08:00:00.000Z", "endsAt": "2026-06-01T17:00:00.000Z", "votingMode": "in_person", "accessMode": "api", "inPersonRequireAdminLogin": true, "inPersonRequireVoterPassword": false } } } ``` ## Election response fields | Field | Type | Description | |---|---|---| | `title` | `string` | Public title of the election. | | `institution` | `string \| null` | Institution or organization running the election. | | `description` | `string \| null` | Public election description. | | `instructions` | `string \| null` | Instructions shown to voters. | | `status` | `string` | Current election status. Common values include `draft`, `live`, `paused`, `cancelled`, and `archived`. | | `startsAt` | `string \| null` | Election start date/time. | | `endsAt` | `string \| null` | Election end date/time. | | `votingMode` | `remote \| in_person` | Determines whether the election is remote or in-person. | | `accessMode` | `portal \| api` | Determines whether the election is accessed through CrownVote portal or the Developer API. | | `inPersonRequireAdminLogin` | `boolean \| null` | `true` or `false` for in-person elections. `null` for remote elections. | | `inPersonRequireVoterPassword` | `boolean \| null` | `true` or `false` for in-person elections. `null` for remote elections. | --- # Remote voting Remote voting allows voters to authenticate from their own devices using their voter ID and password. ## Remote voting flow 1. Call `GET /election`. 2. Confirm `votingMode` is `remote`. 3. Show voter login form with `voterId` and `password`. 4. Login voter. 5. Render returned ballots. 6. Submit encrypted selections. 7. Optionally check submission. 8. Logout voter or clear local session. ## Login voter ```http POST /api/v1/voter/login HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx { "voterId": "236165", "password": "voter-password" } ``` ## Login response ```json { "status": true, "data": { "accessToken": { "type": "Bearer", "value": "oat_xxxxx", "expiresAt": null }, "voter": { "voterId": "236165", "firstName": "Voter 1", "middleName": null, "lastName": null, "displayName": "Voter 1", "group": null, "status": "active", "hasVoted": false, "votedAt": null, "lastLoginAt": null }, "ballots": [ { "categoryId": "70f74ae6-fb73-41e9-87e0-ece419585ed5", "categoryPosition": 1, "title": "PRESIDENT", "description": null, "instructions": null, "candidates": [ { "name": "Kofi Angel", "image": "https://cdn-f.crownvote.com/elections/...", "bio": null, "manifesto": null, "vote": "encrypted_vote_value" } ] } ] } } ``` ## Ballot rendering rules - Render each item in `ballots` as a ballot category. - Render each item in `candidates` as a candidate option. - Treat `candidate.vote` as an opaque encrypted string. - Do not decode, modify, regenerate, or derive the `vote` value. - When a candidate is selected, submit the selected candidate's `vote` value with the matching `categoryId`. ## Restore voter session ```http GET /api/v1/voter HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx Authorization: Bearer oat_xxxxx ``` ## Submit votes ```http POST /api/v1/voter/submit-votes HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx Authorization: Bearer oat_xxxxx { "selections": [ { "categoryId": "70f74ae6-fb73-41e9-87e0-ece419585ed5", "vote": "encrypted_vote_value_for_candidate" } ] } ``` ## Submit response ```json { "status": true, "message": "Vote submitted successfully.", "data": { "submission": { "status": "pending" } } } ``` ## Get voter submission ```http GET /api/v1/voter/submission HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx Authorization: Bearer oat_xxxxx ``` ## Logout voter ```http POST /api/v1/voter/logout HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx Authorization: Bearer oat_xxxxx ``` --- # In-person voting In-person voting is used for controlled voting centers, institutional voting rooms, polling stations, and supervised voting devices. ## In-person voting flow 1. Call `GET /election`. 2. Confirm `votingMode` is `in_person`. 3. If `inPersonRequireAdminLogin` is `true`, show admin login form. 4. Login admin and store `data.admin.accessToken`. 5. Always show voter token input. 6. If `inPersonRequireVoterPassword` is `true`, show voter password input. 7. Login voter. 8. Render returned ballots. 9. Submit encrypted selections. 10. Clear voter session after each voter. 11. Logout admin when the polling officer is done. ## Admin login Show this form only if: ```js election.votingMode === 'in_person' && election.inPersonRequireAdminLogin === true ``` ```http POST /api/v1/admin/login HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx { "pin": "1234", "password": "admin-password" } ``` ## Admin login response ```json { "status": true, "data": { "admin": { "id": "admin_01", "name": "Polling Officer", "accessToken": "cv_admin_xxxxx" } } } ``` Store the admin token: ```js sessionStorage.setItem('crownvote_admin_token', result.data.admin.accessToken) ``` Send it on supervised requests: ```http X-ADMIN-TOKEN: cv_admin_xxxxx ``` ## Verify admin session ```http GET /api/v1/admin HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx ``` ## Voter login: token only Use this when `inPersonRequireVoterPassword` is `false`. ```http POST /api/v1/voter/login HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx { "token": "VTR-92831" } ``` If `inPersonRequireAdminLogin` is `false`, omit `X-ADMIN-TOKEN`. ## Voter login: token and password Use this when `inPersonRequireVoterPassword` is `true`. ```http POST /api/v1/voter/login HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx { "token": "VTR-92831", "password": "voter-password" } ``` If `inPersonRequireAdminLogin` is `false`, omit `X-ADMIN-TOKEN`. ## Voter login response ```json { "status": true, "data": { "accessToken": { "type": "Bearer", "value": "oat_xxxxx", "expiresAt": null }, "voter": { "voterId": "236165", "displayName": "Voter 1", "status": "active", "hasVoted": false }, "ballots": [ { "categoryId": "70f74ae6-fb73-41e9-87e0-ece419585ed5", "categoryPosition": 1, "title": "PRESIDENT", "description": null, "instructions": null, "candidates": [ { "name": "Kofi Angel", "image": "https://cdn-f.crownvote.com/elections/...", "bio": null, "manifesto": null, "vote": "encrypted_vote_value" } ] } ] } } ``` ## Restore voter session ```http GET /api/v1/voter HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx Authorization: Bearer oat_xxxxx ``` If admin login is not required, omit `X-ADMIN-TOKEN`. ## Submit votes ```http POST /api/v1/voter/submit-votes HTTP/1.1 Host: api.crownvote.com Content-Type: application/json X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx Authorization: Bearer oat_xxxxx { "selections": [ { "categoryId": "70f74ae6-fb73-41e9-87e0-ece419585ed5", "vote": "encrypted_vote_value_for_candidate" } ] } ``` If admin login is not required, omit `X-ADMIN-TOKEN`. ## Logout voter ```http POST /api/v1/voter/logout HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx Authorization: Bearer oat_xxxxx ``` If admin login is not required, omit `X-ADMIN-TOKEN`. ## Logout admin ```http POST /api/v1/admin/logout HTTP/1.1 Host: api.crownvote.com X-PUBLIC-KEY: cv_pub_xxxxx X-ADMIN-TOKEN: cv_admin_xxxxx ``` --- # Ballot submission rules - `X-PUBLIC-KEY` must be sent on every request. - Voter bearer token must be valid for protected voter requests. - `X-ADMIN-TOKEN` must be sent only when admin login is required. - Each submitted `categoryId` must exist in the current ballot payload. - Submit exactly one selection for every required ballot category. - Do not submit the same category more than once. - Submit the encrypted `vote` value exactly as received from CrownVote. - A voter cannot vote more than once. - If `STALE_BALLOT_SUBMISSION` is returned, restore the voter session and reload ballots. --- # Common error codes | Code | Meaning | |---|---| | `ELECTION_NOT_FOUND` | No election could be resolved from the public key. | | `ELECTION_PAUSED` | The election is not currently accepting voting requests. | | `ADMIN_LOGIN_NOT_REQUIRED` | Admin login was attempted for an election that does not require admin supervision. | | `INVALID_ADMIN_CREDENTIALS` | The admin pin or password is incorrect. | | `UNAUTHORIZED` | The request is not authorized. | | `VOTER_AUTH_FAILED` | The voter token is missing, invalid, or expired. | | `INVALID_VOTER_CREDENTIALS` | The voter credentials are incorrect. | | `INVALID_VOTER` | The voter is invalid or cannot vote in this election. | | `ALREADY_VOTED` | The voter has already submitted a ballot. | | `STALE_BALLOT_SUBMISSION` | The submitted selections do not match the current ballot snapshot. | | `DUPLICATE_CATEGORY_SELECTION` | A ballot category was submitted more than once. | | `REQUEST_VALIDATION_FAILED` | The request body failed validation. | | `VOTE_SUBMISSION_FAILED` | The vote submission could not be processed. | | `SUBMISSION_NOT_FOUND` | No submission exists for the voter. | | `NOT_FOUND` | API route or resource was not found. | | `INTERNAL_ERROR` | Internal server error. | | `UNKNOWN` | Uncommon error status. | --- # JavaScript quickstart: remote voting ```js const API_URL = 'https://api.crownvote.com/api/v1' const PUBLIC_KEY = 'cv_pub_xxxxx' async function getElection() { const response = await fetch(`${API_URL}/election`, { headers: { 'X-PUBLIC-KEY': PUBLIC_KEY, }, }) return response.json() } async function loginVoter({ voterId, password }) { const response = await fetch(`${API_URL}/voter/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-PUBLIC-KEY': PUBLIC_KEY, }, body: JSON.stringify({ voterId, password }), }) const result = await response.json() if (result.status) { sessionStorage.setItem('crownvote_voter_token', result.data.accessToken.value) } return result } async function submitVotes(selections) { const token = sessionStorage.getItem('crownvote_voter_token') const response = await fetch(`${API_URL}/voter/submit-votes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-PUBLIC-KEY': PUBLIC_KEY, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ selections }), }) return response.json() } ``` --- # JavaScript quickstart: in-person voting ```js const API_URL = 'https://api.crownvote.com/api/v1' const PUBLIC_KEY = 'cv_pub_xxxxx' let election = null let adminToken = sessionStorage.getItem('crownvote_admin_token') let voterToken = sessionStorage.getItem('crownvote_voter_token') async function getElection() { const response = await fetch(`${API_URL}/election`, { headers: { 'X-PUBLIC-KEY': PUBLIC_KEY, }, }) const result = await response.json() if (result.status) { election = result.data.election } return result } function shouldShowAdminLoginForm() { return election?.votingMode === 'in_person' && election?.inPersonRequireAdminLogin === true } function shouldShowVoterPasswordField() { return election?.votingMode === 'in_person' && election?.inPersonRequireVoterPassword === true } async function loginAdmin({ pin, password }) { const response = await fetch(`${API_URL}/admin/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-PUBLIC-KEY': PUBLIC_KEY, }, body: JSON.stringify({ pin, password }), }) const result = await response.json() if (result.status) { adminToken = result.data.admin.accessToken sessionStorage.setItem('crownvote_admin_token', adminToken) } return result } function getAdminHeaders() { if (!shouldShowAdminLoginForm()) { return {} } return { 'X-ADMIN-TOKEN': adminToken, } } async function loginVoter({ token, password }) { const payload = { token } if (shouldShowVoterPasswordField()) { payload.password = password } const response = await fetch(`${API_URL}/voter/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-PUBLIC-KEY': PUBLIC_KEY, ...getAdminHeaders(), }, body: JSON.stringify(payload), }) const result = await response.json() if (result.status) { voterToken = result.data.accessToken.value sessionStorage.setItem('crownvote_voter_token', voterToken) } return result } async function submitVotes(selections) { const response = await fetch(`${API_URL}/voter/submit-votes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-PUBLIC-KEY': PUBLIC_KEY, ...getAdminHeaders(), Authorization: `Bearer ${voterToken}`, }, body: JSON.stringify({ selections }), }) return response.json() } ``` --- # Starter kits CrownVote API Voting starter kits are being prepared for quick editing and drop-in usage. Planned starter kits: - Remote Voting SPA - In-Person Voting SPA - Tailwind CDN single-file starter - Plain JavaScript fetch integration The starter kits should be available shortly. --- # Recommended docs URLs ```txt https://crownvote.com/docs https://crownvote.com/docs/api https://crownvote.com/docs/api/remote-voting https://crownvote.com/docs/api/in-person-voting https://crownvote.com/docs/api/starter-kits https://crownvote.com/docs/api.md https://crownvote.com/docs/llms.txt ``` # Version ```txt v1 ```