In-Person Voting

Build supervised voting-center experiences.

In-person voting is designed for controlled environments where an election officer may supervise voter access. The election response tells your frontend whether to show the admin login form and whether to request the voter password.

In-person voting flow

1

Resolve election

Read the election configuration and check whether admin login or voter password is required.

2

Prepare access

If admin login is required, show the admin login form and store the returned admin token.

3

Login voter

Authenticate the voter using their voting token, and include the password field only when required.

4

Submit vote

Submit encrypted selections using the voter token and the admin token when required.

Resolve the election context

Start by resolving the election using the public key. For in-person voting, the election response controls whether the frontend should show admin login and voter password fields.

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 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
    }
  }
}

Admin login form

Show the admin login form only when inPersonRequireAdminLogin is true.

Admin token storage

Store the returned admin accessToken in UI state or sessionStorage and send it as X-ADMIN-TOKEN.

Voter password field

Show the voter password field only when inPersonRequireVoterPassword is true.

Voter token field

Always show the voter token field for in-person voter login.

Start an admin session when required

If inPersonRequireAdminLogin is true, show the admin login form. Store the returned accessToken in your UI state and send it as X-ADMIN-TOKEN on supervised requests.

code POST /admin/login
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"
}
code Admin login response
{
  "status": true,
  "data": {
    "admin": {
      "id": "admin_01",
      "name": "Polling Officer",
      "accessToken": "cv_admin_xxxxx"
    }
  }
}

Verify the admin session

Use this endpoint after a refresh to confirm that the voting device still has an active admin session.

code GET /admin
GET /api/v1/admin HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
X-ADMIN-TOKEN: cv_admin_xxxxx

Authenticate a voter

Always provide a field for the voter token. Add a voter password field only when inPersonRequireVoterPassword is true.

code Token only
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"
}
code Token + password
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"
}
code Voter login response
{
  "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 the voter session

Restore the active voter session after a page refresh. Include the admin token when admin login is required.

code GET /voter
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

Submit the voter’s selections

Submit exactly one selection for each ballot category. The encrypted vote value must be sent exactly as returned by CrownVote.

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
X-ADMIN-TOKEN: cv_admin_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"
    }
  }
}

Clear sessions

After each voter completes voting, clear the voter session. When the polling officer is done with the device, clear the admin session too.

code POST /voter/logout
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
code POST /admin/logout
POST /api/v1/admin/logout HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
X-ADMIN-TOKEN: cv_admin_xxxxx

Implementation rules

check_circle Use X-PUBLIC-KEY on every API request.
check_circle Show admin login only when inPersonRequireAdminLogin is true.
check_circle Send X-ADMIN-TOKEN on supervised requests after admin login.
check_circle Show voter password only when inPersonRequireVoterPassword is true.
check_circle Use the voter bearer token for protected voter actions.
check_circle Authenticate one voter at a time on the voting device.
check_circle Clear the voter session after each successful submission.
check_circle Do not alter encrypted vote values before submission.
code Admin not required
{
  "status": false,
  "message": "Admin login is not required for this election",
  "code": "ADMIN_LOGIN_NOT_REQUIRED",
  "errors": []
}
code Invalid admin
{
  "status": false,
  "message": "Invalid admin credentials",
  "code": "INVALID_ADMIN_CREDENTIALS",
  "errors": []
}
code Unauthorized
{
  "status": false,
  "message": "Unauthorized request",
  "code": "UNAUTHORIZED",
  "errors": []
}
code Already voted
{
  "status": false,
  "message": "You have already voted in this election",
  "code": "ALREADY_VOTED",
  "errors": []
}

JavaScript example

A minimal browser-side integration that loads the election, conditionally shows the admin login form, conditionally includes voter password, and submits votes.

code 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 restoreVoter() {
  const response = await fetch(`${API_URL}/voter`, {
    headers: {
      'X-PUBLIC-KEY': PUBLIC_KEY,
      ...getAdminHeaders(),
      Authorization: `Bearer ${voterToken}`,
    },
  })

  return response.json()
}

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()
}

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

  voterToken = null
  sessionStorage.removeItem('crownvote_voter_token')

  return response.json()
}

async function logoutAdmin() {
  const response = await fetch(`${API_URL}/admin/logout`, {
    method: 'POST',
    headers: {
      'X-PUBLIC-KEY': PUBLIC_KEY,
      'X-ADMIN-TOKEN': adminToken,
    },
  })

  adminToken = null
  sessionStorage.removeItem('crownvote_admin_token')

  return response.json()
}

Need voter self-service instead?

Use remote voting when voters should access the election directly from their own devices.