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:readscope 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
idto 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 withfrom=latest. - A change of an endorsement in place arrives as
endorsement.updatedwith the new values; a removal asendorsement.deletedwith 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 }
}