The endorsement stream
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.
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.endorsedby.org/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.endorsedby.org/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
- 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 }
}