Step 6
Submit the vote.
Once the voter has selected candidates, submit the encrypted vote values to CrownVote. The vote values must be sent exactly as received from the ballot payload.
Build selections from state
Convert the selected encrypted vote values into the required API payload.
Validate before sending
Stop submission when the voter has not selected a candidate for every ballot category.
Use bearer token
Submit votes with the voter access token returned during login.
Clear voter session
After a successful vote, remove the voter token and hide the ballot screen.
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"
}
]
}{
"status": true,
"message": "Vote submitted successfully.",
"data": {
"submission": {
"status": "pending"
}
}
} Add submitVote() to index.js.
This function builds the selections payload, validates that every ballot category has a selected candidate, submits the vote, and clears the voter session.
async function submitVote() {
hideNotice()
const selections = state.ballots.map((ballot) => {
return {
categoryId: ballot.categoryId,
vote: state.selections[ballot.categoryId],
}
})
const missingSelection = selections.find((selection) => !selection.vote)
if (missingSelection) {
showNotice('Please select one candidate for every ballot category.')
return
}
elements.submitVote.disabled = true
elements.submitVote.textContent = 'Submitting...'
const response = await fetch(`${API_URL}/voter/submit-votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PUBLIC-KEY': PUBLIC_KEY,
Authorization: `Bearer ${state.accessToken}`,
},
body: JSON.stringify({ selections }),
})
const result = await response.json()
if (!result.status) {
elements.submitVote.disabled = false
elements.submitVote.textContent = 'Submit vote'
showNotice(result.message || 'Vote submission failed.')
return
}
sessionStorage.removeItem('crownvote_voter_token')
elements.ballotScreen.classList.add('hidden')
showNotice('Your vote has been submitted successfully.')
}
elements.submitVote.addEventListener('click', submitVote)Handle common submission errors.
Always display the API message to the voter or polling assistant. If the ballot is stale, reload the session and ask the voter to try again.
{
"status": false,
"code": "ALREADY_VOTED",
"message": "You have already voted in this election",
"errors": []
}
{
"status": false,
"code": "STALE_BALLOT_SUBMISSION",
"message": "Your ballot submission is stale. Please refresh and try again.",
"errors": []
}
{
"status": false,
"code": "REQUEST_VALIDATION_FAILED",
"message": "Request validation failed.",
"errors": []
} Your index.js is now complete.
You now have the full JavaScript needed for a minimal remote voting interface. The next page shows the complete project files together.
const API_URL = 'https://api.crownvote.com/api/v1'
const PUBLIC_KEY = 'cv_pub_xxxxx'
const state = {
election: null,
accessToken: null,
ballots: [],
selections: {},
}
const elements = {
title: document.querySelector('#electionTitle'),
description: document.querySelector('#electionDescription'),
notice: document.querySelector('#notice'),
loginForm: document.querySelector('#loginForm'),
voterId: document.querySelector('#voterId'),
password: document.querySelector('#password'),
ballotScreen: document.querySelector('#ballotScreen'),
voterInfo: document.querySelector('#voterInfo'),
ballots: document.querySelector('#ballots'),
submitVote: document.querySelector('#submitVote'),
}
function showNotice(message) {
elements.notice.textContent = message
elements.notice.classList.remove('hidden')
}
function hideNotice() {
elements.notice.textContent = ''
elements.notice.classList.add('hidden')
}
async function getElection() {
hideNotice()
const response = await fetch(`${API_URL}/election`, {
headers: {
'X-PUBLIC-KEY': PUBLIC_KEY,
},
})
const result = await response.json()
if (!result.status) {
showNotice(result.message || 'Unable to load election.')
return
}
state.election = result.data.election
elements.title.textContent = state.election.title
elements.description.textContent =
state.election.description || state.election.instructions || 'Login to cast your vote.'
if (state.election.votingMode !== 'remote') {
showNotice('This starter guide is for remote voting only.')
return
}
if (state.election.accessMode !== 'api') {
showNotice('This election is not configured for API voting.')
return
}
if (state.election.status !== 'live') {
showNotice('This election is not currently live.')
return
}
elements.loginForm.classList.remove('hidden')
}
async function loginVoter(event) {
event.preventDefault()
hideNotice()
const voterId = elements.voterId.value.trim()
const password = elements.password.value.trim()
if (!voterId || !password) {
showNotice('Enter your voter ID and password.')
return
}
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) {
showNotice(result.message || 'Voter login failed.')
return
}
state.accessToken = result.data.accessToken.value
state.ballots = result.data.ballots
state.selections = {}
sessionStorage.setItem('crownvote_voter_token', state.accessToken)
elements.loginForm.classList.add('hidden')
elements.ballotScreen.classList.remove('hidden')
elements.voterInfo.textContent = `Voting as ${result.data.voter.displayName}`
renderBallots()
}
function renderBallots() {
elements.ballots.innerHTML = ''
state.selections = {}
state.ballots.forEach((ballot) => {
const ballotElement = document.createElement('section')
ballotElement.className = 'ballot'
const title = document.createElement('h2')
title.textContent = ballot.title
const instruction = document.createElement('p')
instruction.className = 'muted'
instruction.textContent = ballot.instructions || 'Select one candidate.'
const candidates = document.createElement('div')
candidates.className = 'candidates'
ballot.candidates.forEach((candidate) => {
const label = document.createElement('label')
label.className = 'candidate'
const input = document.createElement('input')
input.type = 'radio'
input.name = ballot.categoryId
input.value = candidate.vote
input.addEventListener('change', () => {
state.selections[ballot.categoryId] = candidate.vote
})
const name = document.createElement('span')
name.textContent = candidate.name
label.appendChild(input)
label.appendChild(name)
candidates.appendChild(label)
})
ballotElement.appendChild(title)
ballotElement.appendChild(instruction)
ballotElement.appendChild(candidates)
elements.ballots.appendChild(ballotElement)
})
}
async function submitVote() {
hideNotice()
const selections = state.ballots.map((ballot) => {
return {
categoryId: ballot.categoryId,
vote: state.selections[ballot.categoryId],
}
})
const missingSelection = selections.find((selection) => !selection.vote)
if (missingSelection) {
showNotice('Please select one candidate for every ballot category.')
return
}
elements.submitVote.disabled = true
elements.submitVote.textContent = 'Submitting...'
const response = await fetch(`${API_URL}/voter/submit-votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PUBLIC-KEY': PUBLIC_KEY,
Authorization: `Bearer ${state.accessToken}`,
},
body: JSON.stringify({ selections }),
})
const result = await response.json()
if (!result.status) {
elements.submitVote.disabled = false
elements.submitVote.textContent = 'Submit vote'
showNotice(result.message || 'Vote submission failed.')
return
}
sessionStorage.removeItem('crownvote_voter_token')
elements.ballotScreen.classList.add('hidden')
showNotice('Your vote has been submitted successfully.')
}
elements.loginForm.addEventListener('submit', loginVoter)
elements.submitVote.addEventListener('click', submitVote)
getElection()What you have now
Your app can now load an election, authenticate a voter, display ballots, collect encrypted selections, and submit the vote to CrownVote.