arrow_backDocs

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.

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"
    }
  ]
}
code Submit response
{
  "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.

code Add to index.js
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.

code Common submission errors
{
  "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.

code index.js
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()
task_alt

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.