VoterGuideOS
Getting started

Record user choices

Capture and persist ballot choices from your users.

In addition to displaying ballot information, the VoterGuideOS platform supports an interactive layer. Readers are able to record their candidate choices so they can reference them on Election Day.

Choices are recorded through the Choices API. They are then retrieved and accessed through the standard Ballots API.

Our Choices API handles complex edge-cases like multi-choice voting, retention elections, and ranked-choice voting.


Background: Ballot Progress

As a user makes choices, the VoterGuideOS API calculates how far they've made it through their ballot preparation. That information is available through the ballot.progress field.

type BallotProgress = {
  racesTotal: number;
  measuresTotal: number;
  racesComplete: number;
  measuresComplete: number;
  percentageComplete: number;
};
{
  "progress": {
    "racesTotal": 4,
    "measuresTotal": 2,
    "racesComplete": 1,
    "measuresComplete": 0,
    "percentageComplete": 16.67
  }
}

This progress field can be used to show a dynamic indicator of how prepared a user is for the election. It handles edge-cases like contested races.

Ballot progress indicator showing how many choices a voter has completed

 


Making your first choice

Generate UUID Lock

In order to make a choice, you'll first need to generate a UUID Lock. UUID locks help to guard sensitive voter information from being accessed from different devices.

Generate one UUID lock per local voter session and store it on the device.

const uuidLock =
  localStorage.getItem('uuidLock') ?? crypto.randomUUID();

localStorage.setItem('uuidLock', uuidLock);

Reuse this value when saving choices and when later retrieving the ballot by ID. For the underlying concept, see User choices and UUID locks.

Save a Race Choice

To save a candidate selection, call the Choices API with the ballot ID, race ID, candidate ID, and uuidLock.

Basic race choice interface showing a selected candidate

 

const response = await fetch(
  "https://www.branch.vote/api/v1/choices",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": "org_123",
    },
    body: JSON.stringify({
      ballot: ballotId,
      race: raceId,
      candidate: candidateId,
      uuidLock
    })
  }
);

const result = await response.json();
import requests

response = requests.post(
    "https://www.branch.vote/api/v1/choices",
    headers={
        "X-Organization-Id": "org_123",
        "Content-Type": "application/json",
    },
    json={
        "ballot": ballot_id,
        "race": race_id,
        "candidate": candidate_id,
        "uuidLock": uuid_lock,
    },
)

result = response.json()
curl -X POST "https://www.branch.vote/api/v1/choices" \
  -H "X-Organization-Id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "ballot": "ballot_123",
    "race": "race_123",
    "candidate": "candidate_123",
    "uuidLock": "local-choice-lock"
  }'

If a matching choice already exists for the same ballot, race, and lock, the API replaces it.

Save a Measure Choice

For ballot measures, send measure and measureChoice instead of race and candidate.

const response = await fetch(
  "https://www.branch.vote/api/v1/choices",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": "org_123",
    },
    body: JSON.stringify({
      ballot: ballotId,
      measure: measureId,
      measureChoice: "yes",
      uuidLock
    })
  }
);

const result = await response.json();
import requests

response = requests.post(
    "https://www.branch.vote/api/v1/choices",
    headers={
        "X-Organization-Id": "org_123",
        "Content-Type": "application/json",
    },
    json={
        "ballot": ballot_id,
        "measure": measure_id,
        "measureChoice": "yes",
        "uuidLock": uuid_lock,
    },
)

result = response.json()
curl -X POST "https://www.branch.vote/api/v1/choices" \
  -H "X-Organization-Id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "ballot": "ballot_123",
    "measure": "measure_123",
    "measureChoice": "yes",
    "uuidLock": "local-choice-lock"
  }'

Use 'yes' or 'no' for measureChoice.


Choice edge-cases

Save Ranked or Multi-Choice Races

Some races allow more than one candidate selection.

For multi-choice races, send candidate as an array of candidate IDs.

const response = await fetch(
  "https://www.branch.vote/api/v1/choices",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": "org_123",
    },
    body: JSON.stringify({
      ballot: ballotId,
      race: raceId,
      candidate: [candidateId1, candidateId2],
      uuidLock
    })
  }
);

const result = await response.json();
import requests

response = requests.post(
    "https://www.branch.vote/api/v1/choices",
    headers={
        "X-Organization-Id": "org_123",
        "Content-Type": "application/json",
    },
    json={
        "ballot": ballot_id,
        "race": race_id,
        "candidate": [candidate_id_1, candidate_id_2],
        "uuidLock": uuid_lock,
    },
)

result = response.json()
curl -X POST "https://www.branch.vote/api/v1/choices" \
  -H "X-Organization-Id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "ballot": "ballot_123",
    "race": "race_123",
    "candidate": ["candidate_123", "candidate_456"],
    "uuidLock": "local-choice-lock"
  }'

For ranked choice races, the same array order represents the voter's ranking.

Ranked choice interface showing first and second choice selections

 

const response = await fetch(
  "https://www.branch.vote/api/v1/choices",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": "org_123",
    },
    body: JSON.stringify({
      ballot: ballotId,
      race: raceId,
      candidate: [firstChoiceCandidateId, secondChoiceCandidateId],
      uuidLock
    })
  }
);

const result = await response.json();
import requests

response = requests.post(
    "https://www.branch.vote/api/v1/choices",
    headers={
        "X-Organization-Id": "org_123",
        "Content-Type": "application/json",
    },
    json={
        "ballot": ballot_id,
        "race": race_id,
        "candidate": [first_choice_candidate_id, second_choice_candidate_id],
        "uuidLock": uuid_lock,
    },
)

result = response.json()
curl -X POST "https://www.branch.vote/api/v1/choices" \
  -H "X-Organization-Id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "ballot": "ballot_123",
    "race": "race_123",
    "candidate": ["first_choice_candidate_123", "second_choice_candidate_456"],
    "uuidLock": "local-choice-lock"
  }'

The API validates the number of selections against the race configuration.


Save a Retention Vote

Retention elections require both the candidate and the voter's yes/no retention choice.

const response = await fetch(
  "https://www.branch.vote/api/v1/choices",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": "org_123",
    },
    body: JSON.stringify({
      ballot: ballotId,
      race: raceId,
      candidate: candidateId,
      retentionChoice: "yes",
      uuidLock
    })
  }
);

const result = await response.json();
import requests

response = requests.post(
    "https://www.branch.vote/api/v1/choices",
    headers={
        "X-Organization-Id": "org_123",
        "Content-Type": "application/json",
    },
    json={
        "ballot": ballot_id,
        "race": race_id,
        "candidate": candidate_id,
        "retentionChoice": "yes",
        "uuidLock": uuid_lock,
    },
)

result = response.json()
curl -X POST "https://www.branch.vote/api/v1/choices" \
  -H "X-Organization-Id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "ballot": "ballot_123",
    "race": "race_123",
    "candidate": "candidate_123",
    "retentionChoice": "yes",
    "uuidLock": "local-choice-lock"
  }'

Use 'yes' or 'no' for retentionChoice.

On this page