Stream API reference
Every endorsement added, changed or removed, as Server-Sent Events or by polling, and the saved filters that choose them.
Ballots, endorsers and endorsements themselves are in the API reference. For how the stream works end to end, see Live stream (SSE and polling).
Endpoints
GET /v1/stream: hold a connection open and receive each change as a Server-Sent Event, seconds after it happens.GET /v1/events: poll for the same changes a page at a time, for servers that can’t hold a connection open (cron jobs, serverless functions, strict firewalls). Addwaitto long-poll./v1/subscriptions: save a filter once and use it by name on either endpoint.
- Base URL
https://api.ballotdata.net- Authentication
Authorization: Bearer ebk_…on every request (orX-API-Key: ebk_…). The key needs thestream:readscope, andsubscriptions:writeto change subscriptions. Keys come from the console; your plan must include the live stream.- Client libraries
npm install ballotdataorpip install ballotdata. The JavaScript and Python samples use them; see client libraries.- Format
text/event-streamfor/v1/stream; JSON over HTTPS for everything else. Times are ISO 8601 in UTC.- Service ends
- The Endorsement Stream, and with it
/v1/streamand/v1/events, ends at the close of December 31, 2027 (Pacific), as do the data endpoints in the API reference. - OpenAPI
- These endpoints are in the same openapi.json (OpenAPI 3.1.0, version 1.3.0) as the rest of the API.
Cursors
A cursor is your position in the stream. Every event carries an id that is also a cursor, and every /v1/events response has a next_cursor, even when it has no events. Store the last one you handled and send it back as cursor (or as the Last-Event-ID header on /v1/stream, which EventSource does by itself) to carry on where you stopped, with nothing missed and nothing repeated.
- Where to start without one. The default,
from=latest, starts at the newest change.from=earlieststarts at the oldest change the stream still holds, andsince=<ISO time>at the first change from that time. - Opaque and portable. Treat a cursor as a string; don’t build or change one. It works on any connection and on either endpoint, so you can switch between streaming and polling.
- Your filter moves it too. Changes your filter skips still move your position: the stream sends
checkpointevents for them, andnext_cursoralready includes them. - Paging.
/v1/eventsreturns up tolimitevents (default 100, at most 1,000), oldest first.has_more: truemeans more are waiting: call again right away withnext_cursor. Withwait(up to 25 seconds), a call with nothing new stays open until a matching change arrives. - How long a cursor lasts. The stream keeps the last 7 days of changes (at most 50,000). A cursor older than that, or a
sincebefore it, gets410 cursor_expired.
Filters
Pass subscription=<id or name> for a saved filter, or put the filter in the query. Different fields combine with AND; comma-separated values within a field with OR.
Errors
Errors work as in the rest of the API (see keys and errors). There are no request rate limits: open as many streams as you need. A cursor older than the stream keeps gets 410 cursor_expired: catch up with GET /v1/endorsements, then start again with from=latest.
Example responses are illustrative: the shapes are exact, the people and picks are made up.
curl "https://api.ballotdata.net/v1/events?subscription=CA%20labor&cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ&wait=20" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"{
"error": {
"code": "cursor_expired",
"message": "That position is older than the stream keeps. Catch up with GET /v1/endorsements, then start again with from=latest.",
"request_id": "8c1f2a4e-6b7d-4c3e-9f10-2a3b4c5d6e7f"
}
}Stream
Every endorsement added, changed or removed, as Server-Sent Events or by polling. Needs the stream:read scope and a plan that includes the stream.
Stream endorsement changes (SSE)
GET /v1/stream
Every endorsement change matching a subscription or inline filter, as Server-Sent Events, within seconds of it happening. Events: ready (first, with the subscription name), endorsement (an Event), checkpoint (moves your position past changes your filter skipped), reconnect (we end connections after an hour), and error (cursor_expired). A comment line arrives every 20 seconds.
Each event id is a cursor: reconnect with Last-Event-ID (EventSource does this itself) or ?cursor= to continue. Cursors work on /v1/events too. Without one you start at the newest change (from=latest); from=earliest or since= start further back. A cursor older than the stream keeps (7 days, at most 50,000 changes) ends the stream with an error event, cursor_expired, or gets 410 before it opens. There is no limit on how many streams you hold open.
The Endorsement Stream ends at the close of December 31, 2027 (Pacific).
Query parameters
subscriptionstringA saved subscription’s id or name (GET /v1/subscriptions). Use it instead of the filter parameters below, not together with them.
ExampleCA laborcategoriescomma-separated stringsEndorser category slugs (GET /v1/endorser-categories).
Examplelaborendorserscomma-separated stringsEndorser slugs (GET /v1/endorsers).
Examplesf-labor-councilendorser_statescomma-separated stringsEndorsers’ home states (USPS codes), or national for endorsers with none.
ExampleCA,nationalstatescomma-separated stringsStates the contests are voted in (USPS codes).
ExampleCAelectionscomma-separated stringsElection slugs (GET /v1/elections).
Exampleca-2026-11-03-generalcontest_kindscomma-separated stringsContest kinds: candidate (an office), measure, or retention (whether to keep a judge).
One ofcandidate,measure,retention.Examplemeasurelevelscomma-separated stringsLevels of the contest’s district, from federal down to school and special districts.
One offederal,state,county,city,school,special.Examplecounty,citytypescomma-separated stringsKinds of change: created (a new endorsement), updated (one that changed) or deleted (one withdrawn or removed). All three by default.
One ofcreated,updated,deleted.Examplecreated,updatedcursorstringResume after this position: a next_cursor from /v1/events or an event id from /v1/stream (either works on both). Also read from the Last-Event-ID header. Takes precedence over since and from. Opaque; valid for 7 days.
Examplec1.eyJzIjp7IjAiOiIxODM0MSJ9fQfromstringWhere to start without a cursor or since: latest (only changes from now on) or earliest (the oldest change the stream still holds, up to 7 days back).
One oflatest,earliest.Defaultlatest.Exampleearliestsincestring (date-time)Start at the first change at or after this time (ISO 8601). Must be within the last 7 days, or you get 410 cursor_expired. Use instead of cursor.
Example2026-10-27T00:00:00Z
Returns
200 An open event stream.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.404Nothing has that id or slug.410The cursor is older than the stream keeps.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.503Not ready yet on this server. Wait Retry-After seconds.
curl -N "https://api.ballotdata.net/v1/stream?subscription=CA%20labor" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"# Instead of the filters: subscription=CA labor
# Instead of cursor: from=earliest or since=2026-10-27T00:00:00Z
curl -N -G "https://api.ballotdata.net/v1/stream" \
--data-urlencode "categories=labor" \
--data-urlencode "endorsers=sf-labor-council" \
--data-urlencode "endorser_states=CA,national" \
--data-urlencode "states=CA" \
--data-urlencode "elections=ca-2026-11-03-general" \
--data-urlencode "contest_kinds=measure" \
--data-urlencode "levels=county,city" \
--data-urlencode "types=created,updated" \
--data-urlencode "cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"retry: 5000
id: c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ
event: ready
data: {"subscription":"CA labor"}
id: c1.eyJzIjp7IjAiOiIxODM0MiJ9fQ
event: endorsement
data: {"id":"18342","type":"endorsement.created","occurred_at":"2026-10-14T21:07:33.512Z","endorsement":{"id":90211,"rank":1,"statement":null,"source_url":"https://sflaborcouncil.org/endorsements","published_on":"2026-09-18","added_by":"data_load"},"changed":[],"previous":null,"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,"incumbent":true}}
: ping
id: c1.eyJzIjp7IjAiOiIxODM1MCJ9fQ
event: checkpoint
data: {}
Poll endorsement changes
GET /v1/events
The same changes as GET /v1/stream, a page at a time, for servers that can’t hold a connection open (cron jobs, serverless functions, strict firewalls). Each response has the changes after your cursor that match your filter, oldest first, and a next_cursor (even when there are none): store it and send it as cursor next time. has_more means more are waiting, so call again right away. wait (up to 25 seconds) holds a request with nothing new open until a matching change arrives (long polling).
Without a cursor you start at the newest change (from=latest); from=earliest or since= start further back. Cursors work on /v1/stream too. A cursor older than the stream keeps (7 days, at most 50,000 changes) gets 410 cursor_expired: catch up with GET /v1/endorsements, then start again with from=latest.
The Endorsement Stream ends at the close of December 31, 2027 (Pacific).
Query parameters
subscriptionstringA saved subscription’s id or name (GET /v1/subscriptions). Use it instead of the filter parameters below, not together with them.
ExampleCA laborcategoriescomma-separated stringsEndorser category slugs (GET /v1/endorser-categories).
Examplelaborendorserscomma-separated stringsEndorser slugs (GET /v1/endorsers).
Examplesf-labor-councilendorser_statescomma-separated stringsEndorsers’ home states (USPS codes), or national for endorsers with none.
ExampleCA,nationalstatescomma-separated stringsStates the contests are voted in (USPS codes).
ExampleCAelectionscomma-separated stringsElection slugs (GET /v1/elections).
Exampleca-2026-11-03-generalcontest_kindscomma-separated stringsContest kinds: candidate (an office), measure, or retention (whether to keep a judge).
One ofcandidate,measure,retention.Examplemeasurelevelscomma-separated stringsLevels of the contest’s district, from federal down to school and special districts.
One offederal,state,county,city,school,special.Examplecounty,citytypescomma-separated stringsKinds of change: created (a new endorsement), updated (one that changed) or deleted (one withdrawn or removed). All three by default.
One ofcreated,updated,deleted.Examplecreated,updatedcursorstringResume after this position: a next_cursor from /v1/events or an event id from /v1/stream (either works on both). Also read from the Last-Event-ID header. Takes precedence over since and from. Opaque; valid for 7 days.
Examplec1.eyJzIjp7IjAiOiIxODM0MSJ9fQfromstringWhere to start without a cursor or since: latest (only changes from now on) or earliest (the oldest change the stream still holds, up to 7 days back).
One oflatest,earliest.Defaultlatest.Exampleearliestsincestring (date-time)Start at the first change at or after this time (ISO 8601). Must be within the last 7 days, or you get 410 cursor_expired. Use instead of cursor.
Example2026-10-27T00:00:00ZlimitintegerEvents per page, 1–1000. has_more in the response says more are waiting.
Default100.1 to 1000.Example500waitintegerSeconds to hold the request open when nothing new matches (long polling), 0–25. 0 answers at once.
Default0.0 to 25.Example20
Returns
200 Events and the next cursor.
dataarray of EventChanges, oldest first.
10 fields
idstringThe change’s id: increasing and unique. Use it to drop repeats.
typestringWhat happened.
One ofendorsement.created,endorsement.updated,endorsement.deleted.occurred_atstring (date-time)When the change was made.
endorsementobject6 fields
idintegerEndorsement id.
rankinteger or nullRanked choice rank.
statementstring or nullWhat the endorser said.
source_urlstring or nullWhere it was published.
published_onstring (date) or nullWhen.
added_bystringHow the endorsement got here: automatic (added by our automatic watch of the endorser’s own page), staff (entered by hand by BallotData staff) or data_load (loaded in a researched data pass).
One ofautomatic,staff,data_load.
changedarray of stringFor endorsement.updated: the fields that changed. Empty for other events.
One ofchoice_id,rank,statement,source_url,published_on.previousobject or nullFor endorsement.updated: the values before the change. null for other events.
5 fields
choice_idintegerThe choice endorsed before.
rankinteger or nullRank before.
statementstring or nullStatement before.
source_urlstring or nullSource before.
published_onstring (date) or nullDate before.
endorserobject5 fields
idintegerEndorser id.
slugstring or nullEndorser slug.
namestring or nullEndorser name.
categoryCategory or nullCategory.
2 fields
slugstringCategory slug, used in categories= filters.
namestringDisplay name.
statestring or nullHome state, or null for national.
contestobject9 fields
idintegerContest id.
titlestring or nullTitle.
short_titlestring or nullShort title.
kindstring or nullcandidate, measure or retention.
vote_methodstring or nullVote method.
votes_allowedinteger or nullVote for N.
ranks_allowedinteger or nullRanks allowed.
measure_codestring or nullMeasure code.
districtobject or nullDistrict.
5 fields
idintegerDistrict id.
namestringName.
levelstringLevel.
ocd_idstring or nullOCD id.
statestring or nullState.
electionobject or nullElection.
6 fields
idintegerElection id.
slugstringSlug.
namestringName.
datestring (date)Date.
statestring or nullState.
typestringType.
choiceobject7 fields
idintegerChoice id.
labelstring or nullAs printed.
measure_answerstring or nullyes or no.
One ofyes,no.candidateobject or nullCandidate.
2 fields
idintegerCandidate id.
full_namestringFull name.
partyobject or nullParty.
2 fields
slugstringSlug.
namestringName.
withdrawnboolean or nullWithdrawn.
incumbentboolean or nullThe candidate holds this seat now: true or false where we know, null where we don’t (and for a measure answer).
next_cursorstringSend as cursor= next time.
has_morebooleanMore are waiting: call again now.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.404Nothing has that id or slug.410The cursor is older than the stream keeps.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.503Not ready yet on this server. Wait Retry-After seconds.
curl "https://api.ballotdata.net/v1/events?subscription=CA+labor&cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ&wait=20" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"# Instead of the filters: subscription=CA labor
# Instead of cursor: from=earliest or since=2026-10-27T00:00:00Z
curl -G "https://api.ballotdata.net/v1/events" \
--data-urlencode "categories=labor" \
--data-urlencode "endorsers=sf-labor-council" \
--data-urlencode "endorser_states=CA,national" \
--data-urlencode "states=CA" \
--data-urlencode "elections=ca-2026-11-03-general" \
--data-urlencode "contest_kinds=measure" \
--data-urlencode "levels=county,city" \
--data-urlencode "types=created,updated" \
--data-urlencode "cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ" \
--data-urlencode "limit=500" \
--data-urlencode "wait=20" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"{
"data": [
{
"id": "18342",
"type": "endorsement.created",
"occurred_at": "2026-10-14T21:07:33.512Z",
"endorsement": {
"id": 90211,
"rank": 1,
"statement": null,
"source_url": "https://sflaborcouncil.org/endorsements",
"published_on": "2026-09-18",
"added_by": "data_load"
},
"changed": [],
"previous": null,
"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,
"incumbent": true
}
}
],
"next_cursor": "c1.eyJzIjp7IjAiOiIxODM0MiJ9fQ",
"has_more": false
}Subscriptions
Saved filters for the stream and events. Reading needs stream:read; changing needs subscriptions:write.
List subscriptions
GET /v1/subscriptions
Your account’s saved subscriptions, by name.
Returns
200 Every subscription.
dataarray of SubscriptionEvery item.
5 fields
idintegerSubscription id.
namestringName, unique in your account.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
created_atstring (date-time)Created.
updated_atstring (date-time)Last changed.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
curl "https://api.ballotdata.net/v1/subscriptions" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"{
"data": [
{
"id": 7,
"name": "CA labor",
"filter": {
"categories": ["labor"],
"states": ["CA"]
},
"created_at": "2026-10-15T17:20:04.112Z",
"updated_at": "2026-10-15T17:20:04.112Z"
}
]
}Create a subscription
POST /v1/subscriptions
Save a named filter. Use it as subscription=<id or name> on /v1/stream and /v1/events. An account can have up to 50.
Request body
JSON.
namestring1–100 letters, digits, spaces, dots, dashes or underscores.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
Returns
201 Created.
dataSubscription5 fields
idintegerSubscription id.
namestringName, unique in your account.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
created_atstring (date-time)Created.
updated_atstring (date-time)Last changed.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.409A subscription with that name already exists.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
curl -X POST "https://api.ballotdata.net/v1/subscriptions" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"CA labor","filter":{"categories":["labor"],"states":["CA"]}}'{
"data": {
"id": 7,
"name": "CA labor",
"filter": {
"categories": ["labor"],
"states": ["CA"]
},
"created_at": "2026-10-15T17:20:04.112Z",
"updated_at": "2026-10-15T17:20:04.112Z"
}
}Get a subscription
GET /v1/subscriptions/{id}
One saved subscription.
Path parameters
idintegerrequiredSubscription id (from GET /v1/subscriptions).
Example7
Returns
200 The subscription.
dataSubscription5 fields
idintegerSubscription id.
namestringName, unique in your account.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
created_atstring (date-time)Created.
updated_atstring (date-time)Last changed.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.404Nothing has that id or slug.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
curl "https://api.ballotdata.net/v1/subscriptions/7" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"{
"data": {
"id": 7,
"name": "CA labor",
"filter": {
"categories": ["labor"],
"states": ["CA"]
},
"created_at": "2026-10-15T17:20:04.112Z",
"updated_at": "2026-10-15T17:20:04.112Z"
}
}Replace a subscription
PUT /v1/subscriptions/{id}
Replace a subscription’s name and filter. Open streams using it keep their old filter until they reconnect.
Path parameters
idintegerrequiredSubscription id (from GET /v1/subscriptions).
Example7
Request body
JSON.
namestring1–100 letters, digits, spaces, dots, dashes or underscores.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
Returns
200 Saved.
dataSubscription5 fields
idintegerSubscription id.
namestringName, unique in your account.
filterFilterWhich endorsements: fields combine with AND, values within a field with OR, and a missing field matches everything. Each field takes at most 500 values.
8 fields
categoriesarray of stringEndorser category slugs (GET /v1/endorser-categories).
endorsersarray of stringEndorser slugs.
endorser_statesarray of stringEndorsers’ home states (USPS codes), or "national".
statesarray of stringStates the contests are voted in.
electionsarray of stringElection slugs.
contest_kindsarray of stringContest kinds.
One ofcandidate,measure,retention.levelsarray of stringDistrict levels.
One offederal,state,county,city,school,special.typesarray of stringStream only: kinds of change.
One ofcreated,updated,deleted.
created_atstring (date-time)Created.
updated_atstring (date-time)Last changed.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.404Nothing has that id or slug.409A subscription with that name already exists.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
curl -X PUT "https://api.ballotdata.net/v1/subscriptions/7" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"CA labor","filter":{"categories":["labor"],"states":["CA","NV"]}}'{
"data": {
"id": 7,
"name": "CA labor",
"filter": {
"categories": ["labor"],
"states": ["CA", "NV"]
},
"created_at": "2026-10-15T17:20:04.112Z",
"updated_at": "2026-10-21T09:12:40.006Z"
}
}Delete a subscription
DELETE /v1/subscriptions/{id}
Delete a subscription.
Path parameters
idintegerrequiredSubscription id (from GET /v1/subscriptions).
Example7
Returns
204 Deleted. No body.
Errors
400A parameter is missing or not valid.401No API key, or one that isn’t valid, was revoked or expired.403The key can’t be used for this: a missing scope, an IP allowlist, an account not yet approved, or data the account hasn’t bought.404Nothing has that id or slug.429Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
curl -X DELETE "https://api.ballotdata.net/v1/subscriptions/7" \
-H "Authorization: Bearer $BALLOTDATA_API_KEY"(no body)