Elections, contests and endorsements

Everything about the races on the ballot and who endorses what. For the district a contest is in and who holds each seat now, see Contests and officeholders.

Endpoints

EndpointWhat
GET /v1/statesStates, with FIPS codes
GET /v1/electionsElections, with their voting rules in details and how many contests have results (result_count). Past elections kept for their results have details.historical. states, types, from, to (YYYY-MM-DD), upcoming=true
GET /v1/endorser-categoriesKinds of endorser: labor, newspaper, party, advocacy…
GET /v1/endorsersEndorsers, each with endorsement_count (contests they’ve taken a position in). categories, states (home state, or national), elections, q, min_endorsements
GET /v1/endorsers/{slug}One endorser and the elections they’ve endorsed in
GET /v1/contestsContests with their choices and results. elections, states, levels, contest_kinds, districts (OCD ids), upcoming, has_result, result_statuses
GET /v1/contests/{id}One contest with its result and every endorser’s position. categories, endorsers
GET /v1/endorsementsEndorsements, flat, with the basics of each: endorser, contest, election, choice and rank. categories, endorsers, endorser_states, elections, states, contest_kinds, levels, contest, changed_since, upcoming
GET /v1/endorsements/{id}One endorsement in full: the statement and source, the endorser, the contest with its district and election, and the choice with its candidate
GET /v1/endorsements/{id}/historyEvery change to one endorsement, with the values before and after, including its deletion
GET /v1/changesEvery endorsement added, changed or deleted since a time, with the values before and after. since, categories, endorsers, elections, states
GET /v1/candidatesCandidates, with the contests they’re on, how many endorsers backed them in each, and their votes once there’s a result. q, states, elections, incumbent, upcoming
GET /v1/candidates/{id}One candidate with every endorsement they received, and where they stand on common issues (issue_positions)
GET /v1/issuesThe issues we record candidates’ positions on, each a yes/no question a position’s stance answers
GET /v1/issue-positionsWhere candidates stand, one row per candidate and issue, each with a summary, their own words where we have them, and its sources. issues, stances, candidates, contests, elections, states
GET /v1/elections/{slug}/exportA whole election in one download: every contest, choice, result and position as JSON, or one row per endorsement with format=csv (with the endorsed choice’s votes and whether it won)
GET /v1/coveragePer state: elections, contests, measures, endorsers, endorsements, and when they last changed. upcoming=false counts past elections too

List parameters take comma-separated values: categories=labor,newspaper.

Ballot details

Contests carry details where they apply: uncontested, nonpartisan, term, short_term, vote_required and majority_required for measures, amount for bonds, runoff_date, elected_at_large, none_of_these and more, plus the official source_url. Elections carry voting rules: all_mail, ranked_choice, fusion_voting, runoff_date. A choice has party_label where the ballot prints one (Washington’s “Prefers Democratic Party”), and option: "none_of_these" for Nevada’s “None of these candidates”. Add lang=es to any data endpoint for Spanish ballot text where the state publishes it.

Measures carry their wording as printed on the ballot in ballot_text (null where we don’t have it yet), a link to the official full text in full_text_url, and on each Yes and No choice, what that vote means in vote_meaning where the election office publishes it. Each candidate choice says whether the candidate is the incumbent: true or false where we know, null where we don’t yet. A contest is unopposed when no more candidates are running than there are seats.

Every contest we list is on the printed ballot. Where a state leaves unopposed races off the ballot (South Dakota’s county offices, for example), so do we.

Election results

Once a source publishes a contest’s result, the contest carries result and each choice carries result with its votes, vote_pct (0 to 100) and winner. Before then both are null. A result’s status is unofficial while counts are still changing and certified once the canvass is certified; we read unofficial results again every day until they’re certified.

Every result names its source and links to it (source.url). source_kind says what kind of source it is:

  • official: a state, county, city or federal election office.
  • compiled: certified official returns compiled by a research group (the MIT Election Data and Science Lab’s precinct returns) and summed to the contest. Used for past elections such as November 5, 2024; official_url links to the state’s own certified results where we have them.
  • party: a state or national party’s announcement of the winner, for contests no official source covers yet. These have no vote counts (votes is null) and set party_reported: true. We replace them with the official result as soon as one appears.

winner is set only when the source says so, or when the result is certified in a vote-for-one or vote-for-N race (the top N). It stays null for races headed to a runoff. Ranked-choice contests carry the official tabulation’s rounds in rcv_rounds. Write-ins and other lines that aren’t on the printed ballot are in other_choices and write_in_votes, so the choices’ votes may add up to less than total_votes.

Past elections (the November 5, 2024 general in every state) are ordinary elections with details.historical: true, listed by GET /v1/elections unless you ask for upcoming=true. Their contests, choices and results work the same way.

Keeping a copy up to date

Load everything once, with GET /v1/elections/{slug}/export for each election or by paging through GET /v1/endorsements. After that, ask GET /v1/changes for every change since your last sync: each one says whether the endorsement was created, updated or deleted, with its values before and after. Keep the next_cursor of the last page and pass it next time, so nothing is missed or repeated.

changed_since on GET /v1/endorsements lists the endorsements added or changed since a time (but not deletions), and GET /v1/endorsements/{id}/history has every change to one endorsement.

How each endorsement got here

Every endorsement has added_by: data_load when it was loaded in one of our researched data passes, staff when BallotData staff entered it by hand, or automatic when our watch of the endorser’s own website added it. The watch only adds a candidate named on the endorser’s own page for that election, never one from a news story or another site. Its source_url is that page.

How positions are shaped

A contest has a vote_method: plurality (vote for one), multi_seat (vote for up to votes_allowed), ranked_choice (rank up to ranks_allowed), approval or yes_no (measures and judicial retention). Each endorser’s position is a list of picks: one choice for vote-for-one; up to N for multi-seat; ranked choices with rank 1, 2, 3… for ranked choice; and the Yes or No choice (measure_answer) for a measure. An endorser with no entry has taken no position.

Address and geography lookups retired

The API no longer looks up places. GET /v1/ballot (the ballot at an address), GET /v1/districts and /v1/districts/{id} (districts and their boundaries) and GET /v1/representatives (who represents a place) have been removed. Choose contests by state, election or contest id instead. Every contest still names its district (id, ocd_id, name, level, type, state), which is all you need to tell races apart.

Elections, contests and endorsements · BallotData API