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 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 }
}