Endorsement Stream docs
Formats and fields
CSV
UTF-8 with a byte-order mark (so Excel reads accents correctly), RFC 4180 with CRLF line ends, one row per endorsement, and the column names on the first line. Every row repeats its election, contest, district, choice and endorser, so it needs no joins. Columns:
endorsement_id, election_id, election_name, election_date, election_type, state, contest_id, contest_title, contest_type, measure_code, district_id, district_name, district_level, district_type, ocd_id, census_geoid, vote_method, seats, ranks_allowed, party_ballot, choice_id, choice, candidate_id, candidate_name, ballot_designation, choice_party, measure_position, withdrawn, incumbent, endorser_id, endorser_slug, endorser_name, endorser_type, endorser_state, endorser_party, endorser_website, rank, source_url, published_on, retrieved_at
JSON
ballotdata.endorsements/1 keeps the ballot’s structure: a head with coverage, counts, endorser types and endorsers, then elections → contests → choices → endorsements, in ballot order. Each contest carries endorser_count; each endorsement names its endorser by id, name and type. Shortened, it looks like this:
{
"format": "ballotdata.endorsements/1",
"generated_at": "2026-10-16T13:00:00.000Z",
"coverage": { "national": false, "states": ["CA"], "contests": [], "endorser_types": [] },
"counts": { "contests": 412, "choices": 1290, "endorsements": 5120, "endorsers": 380 },
"endorser_types": [{ "slug": "labor", "name": "Labor" }, …],
"endorsers": [{ "id": 77, "slug": "…", "name": "…", "type": { "slug": "advocacy", "name": "Advocacy" }, "state": "CA", … }],
"elections": [{
"id": 12, "slug": "ca-2026-11-03", "name": "November 3, 2026 General", "date": "2026-11-03", "type": "general",
"contests": [{
"id": 9031, "title": "Proposition 5", "type": "measure", "measure_code": "5", "vote_method": "plurality",
"district": { "id": 6, "name": "California", "level": "state", "ocd_id": "ocd-division/country:us/state:ca", … },
"endorser_count": 41,
"choices": [{
"id": 51001, "label": "Yes", "measure_position": "yes", "ballot_order": 1,
"endorsements": [{
"id": 880412, "endorser": { "id": 77, "name": "…", "type": "advocacy" },
"rank": null, "source_url": "https://…", "published_on": "2026-09-30", "retrieved_at": "…"
}]
}, …]
}, …]
}]
}Change files
Emails attach a CSV or JSON of just their changes. A change is endorsement.created, endorsement.updated (with the new values) or endorsement.deleted (with the values it had). The change CSV’s columns are:
change_id, change_type, occurred_at, endorsement_id, election_id, election_name, election_date, state, contest_id, contest_title, contest_type, measure_code, district_name, district_level, vote_method, seats, ranks_allowed, choice_id, choice, candidate_id, choice_party, measure_position, withdrawn, endorser_id, endorser_slug, endorser_name, endorser_type, endorser_state, rank, source_url, published_on
The change JSON, postbacks and the live stream’s events all carry the same change objects. Endorsers’ statements are not in the files; the API serves them.
Identifiers
Districts carry an Open Civic Data division id (ocd_id) and, where the Census publishes the district, a Census GEOID (census_geoid, e.g. cd:0611, county:06075). Ids for elections, contests, choices, candidates, endorsers and endorsements are stable across deliveries and match the API’s.