Endorsement Stream docs

Live stream: Server-Sent Events and polling

Every endorsement added, changed or withdrawn in our database becomes an event on an Amazon Kinesis stream within seconds. You read it through this API, filtered to the endorsements you want, without needing AWS credentials. Hold a connection open with Server-Sent Events, or poll with a cursor; both carry the same events.

Before you start

  • Buy an endorsements plan at ballotdata.net with Live stream as its delivery, and choose your API account’s password at checkout. Already have a plan? Switch its delivery to the stream from your ballotdata.net account.
  • Once it’s paid, your API account opens and we email a link to confirm your address. Confirm it, sign in and create a key with the stream:read scope in the console.
  • The stream carries only changes in the states and contests you bought, for the kinds of endorsers your delivery chose. Keys, limits and errors work as in the API docs.

Choose what you get

Either save a subscription (in the console or with POST /v1/subscriptions) and pass subscription=<id or name>, or put the filter in the query: categories, endorsers, endorser_states, states, elections, contest_kinds, levels, types (created, updated, deleted). Fields combine with AND; values within a field with OR; leave a field out to get everything.

curl -X POST https://api.ballotdata.net/v1/subscriptions \
  -H "Authorization: Bearer ebk_…" -H "Content-Type: application/json" \
  -d '{"name": "CA labor", "filter": {"categories": ["labor"], "states": ["CA"]}}'

Server-Sent Events

curl -N -H "Authorization: Bearer ebk_…" \
  "https://api.ballotdata.net/v1/stream?subscription=CA%20labor"

event: ready
id: c1.eyJzIjp7…
data: {"subscription":"CA labor"}

event: endorsement
id: c1.eyJzIjp7…
data: {"id":"18342","type":"endorsement.created",…}

Each event’s id is a cursor. Reconnect with Last-Event-ID: <id> (EventSource clients do this themselves) or ?cursor= to continue where you stopped. checkpoint events move your position past changes your filter skipped; a comment line arrives every 20 seconds to keep the connection open. We end connections after an hour with a reconnect event; reconnect with the last id. New connections start at the newest change unless you pass cursor, since=<ISO time> or from=earliest. Your plan sets how many connections you can hold open.

Polling

GET /v1/events?subscription=CA%20labor&cursor=c1.…&wait=20
→ { "data": [ …events… ], "next_cursor": "c1.…", "has_more": false }

Store next_cursor and send it next time. wait (up to 25 seconds) holds the request until a matching change arrives. has_more means call again right away.

Delivery guarantees

  • At least once: in rare cases (a retry after a failure) an event can arrive twice. Use its id to drop repeats.
  • In order for each endorser. Changes to different endorsers can interleave.
  • The stream keeps changes for its retention window (a week in production). A cursor older than that gets 410 cursor_expired: catch up with /v1/endorsements, then start again with from=latest.
  • A change of an endorsement in place arrives as endorsement.updated with the new values; a removal as endorsement.deleted with the values it had.

An event

{
  "id": "18342",
  "type": "endorsement.created",
  "occurred_at": "2026-10-14T21:07:33.512Z",
  "endorsement": { "id": 90211, "rank": null, "statement": "…", "source_url": "https://…", "published_on": "2026-10-12" },
  "endorser": { "id": 512, "slug": "sf-labor-council", "name": "San Francisco Labor Council",
                "category": { "slug": "labor", "name": "Labor" }, "state": "CA" },
  "contest": { "id": 4410, "title": "Member, Board of Supervisors, District 8", "short_title": "Supervisor, D8",
               "kind": "candidate", "vote_method": "ranked_choice", "votes_allowed": 1, "ranks_allowed": 10,
               "measure_code": null,
               "district": { "id": 77, "name": "San Francisco Supervisorial District 8", "level": "county",
                             "ocd_id": "ocd-division/country:us/state:ca/county:san_francisco/council_district:8", "state": "CA" } },
  "election": { "id": 3, "slug": "ca-2026-11-03-general", "name": "California General Election", "date": "2026-11-03",
                "state": "CA", "type": "general" },
  "choice": { "id": 20931, "label": "Jane Doe", "measure_answer": null, "candidate": { "id": 8811, "full_name": "Jane Doe" },
              "party": null, "withdrawn": false }
}