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
Resolve election
Read the election configuration and check whether admin login or voter password is required.
Prepare access
If admin login is required, show the admin login form and store the returned admin token.
Login voter
Authenticate the voter using their voting token, and include the password field only when required.
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.
GET /api/v1/election HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx{
"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.
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"
}{
"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.
GET /api/v1/admin HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
X-ADMIN-TOKEN: cv_admin_xxxxxAuthenticate a voter
Always provide a field for the voter token. Add a voter password field only when inPersonRequireVoterPassword is true.
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"
}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"
}{
"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.
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_xxxxxSubmit the voter’s selections
Submit exactly one selection for each ballot category. The encrypted vote value must be sent exactly as returned by CrownVote.
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"
}
]
}{
"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.
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_xxxxxPOST /api/v1/admin/logout HTTP/1.1
Host: api.crownvote.com
X-PUBLIC-KEY: cv_pub_xxxxx
X-ADMIN-TOKEN: cv_admin_xxxxxImplementation rules
{
"status": false,
"message": "Admin login is not required for this election",
"code": "ADMIN_LOGIN_NOT_REQUIRED",
"errors": []
}{
"status": false,
"message": "Invalid admin credentials",
"code": "INVALID_ADMIN_CREDENTIALS",
"errors": []
}{
"status": false,
"message": "Unauthorized request",
"code": "UNAUTHORIZED",
"errors": []
}{
"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.
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()
}