Remote Voting

Let voters cast ballots from your own frontend.

Remote voting uses a voter ID and password to authenticate voters. Once authenticated, CrownVote returns the voter profile, access token, and ballots in a single response.

Remote voting flow

1

Resolve election

Use the public key to confirm the election context before showing the voting form.

2

Login voter

Authenticate the voter with voterId and password. The response includes the ballots.

3

Render ballot

Display categories and candidates from the ballots array. Treat vote values as opaque strings.

4

Submit selection

Submit one encrypted vote value for every ballot category using the voter bearer token.

Resolve the election context

Use the public key to resolve the election before the voter signs in. This confirms that the election is available, remote, and configured for API voting.

code GET /election
GET /api/v1/election HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
code Election response
{
  "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
    }
  }
}

Authenticate the voter

Remote voters authenticate with voterId and password. The response includes an access token and the full ballot payload.

code POST /voter/login
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"
}
code Login response
{
  "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"
          }
        ]
      }
    ]
  }
}

Restore the voter session

Use this endpoint after a page refresh. It returns the voter and ballots again when the bearer token is still valid.

code GET /voter
GET /api/v1/voter HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
Authorization: Bearer oat_xxxxx

Submit encrypted selections

Submit one selection for each ballot category returned during login or session restore. The vote value must be submitted exactly as received.

code POST /voter/submit-votes
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"
    },
    {
      "categoryId": "211c9edf-dfc6-48f5-914c-f3bfc7edeee9",
      "vote": "encrypted_vote_value_for_candidate"
    }
  ]
}
code Accepted response
{
  "status": true,
  "message": "Vote submitted successfully.",
  "data": {
    "submission": {
      "status": "pending"
    }
  }
}

Check latest submission

Use this endpoint to retrieve the voter’s latest submission state after voting.

code GET /voter/submission
GET /api/v1/voter/submission HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
Authorization: Bearer oat_xxxxx
code Submission response
{
  "status": true,
  "data": {
    "submission": {
      "status": "processed",
      "submittedAt": "2026-06-01T16:13:27.000Z",
      "processedAt": "2026-06-01T16:13:28.000Z"
    }
  }
}

End the voter session

Logout clears active voter access tokens. You should also remove the token from local browser storage after a successful vote or logout.

code POST /voter/logout
POST /api/v1/voter/logout HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
Authorization: Bearer oat_xxxxx

Implementation rules

check_circle Use X-PUBLIC-KEY on every API request.
check_circle Use Authorization: Bearer <token> after voter login.
check_circle Submit exactly one selection for every ballot category.
check_circle Do not modify, decode, or regenerate encrypted vote values.
check_circle When STALE_BALLOT_SUBMISSION is returned, refresh the voter session and reload ballots.
check_circle After successful submission, disable the voting form.
code Stale ballot
{
  "status": false,
  "message": "Your ballot submission is stale. Please refresh and try again.",
  "code": "STALE_BALLOT_SUBMISSION",
  "errors": []
}
code Already voted
{
  "status": false,
  "message": "You have already voted in this election",
  "code": "ALREADY_VOTED",
  "errors": []
}
code Validation error
{
  "status": false,
  "message": "Request validation failed.",
  "code": "REQUEST_VALIDATION_FAILED",
  "errors": [
    {
      "field": "selections",
      "message": "The selections field must be defined",
      "rule": "required"
    }
  ]
}

JavaScript example

A minimal browser-side integration for loading the election, logging in, restoring the voter session, and submitting votes.

code 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) {
    localStorage.setItem('crownvote_voter_token', result.data.accessToken.value)
  }

  return result
}

async function restoreVoter() {
  const token = localStorage.getItem('crownvote_voter_token')

  const response = await fetch(`${API_URL}/voter`, {
    headers: {
      'X-PUBLIC-KEY': PUBLIC_KEY,
      Authorization: `Bearer ${token}`,
    },
  })

  return response.json()
}

async function submitVotes(selections) {
  const token = localStorage.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()
}

async function logoutVoter() {
  const token = localStorage.getItem('crownvote_voter_token')

  const response = await fetch(`${API_URL}/voter/logout`, {
    method: 'POST',
    headers: {
      'X-PUBLIC-KEY': PUBLIC_KEY,
      Authorization: `Bearer ${token}`,
    },
  })

  localStorage.removeItem('crownvote_voter_token')

  return response.json()
}

Need controlled voting centers?

Use in-person voting for voter tokens, optional voter passwords, and admin-supervised sessions.