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.