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). Add wait to 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 (or X-API-Key: ebk_…). The key needs the stream:read scope, and subscriptions:write to change subscriptions. Keys come from the console; your plan must include the live stream.
Client libraries
npm install ballotdata or pip install ballotdata. The JavaScript and Python samples use them; see client libraries.
Format
text/event-stream for /v1/stream; JSON over HTTPS for everything else. Times are ISO 8601 in UTC.
Service ends
The Endorsement Stream, and with it /v1/stream and /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=earliest starts at the oldest change the stream still holds, and since=<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 checkpoint events for them, and next_cursor already includes them.
  • Paging. /v1/events returns up to limit events (default 100, at most 1,000), oldest first. has_more: true means more are waiting: call again right away with next_cursor. With wait (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 since before it, gets 410 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.

Poll for changes
curl "https://api.ballotdata.net/v1/events?subscription=CA%20labor&cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ&wait=20" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
Error410
{
  "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

  • subscriptionstring

    A saved subscription’s id or name (GET /v1/subscriptions). Use it instead of the filter parameters below, not together with them.

    Example CA labor
  • categoriescomma-separated strings

    Endorser category slugs (GET /v1/endorser-categories).

    Example labor
  • endorserscomma-separated strings

    Endorser slugs (GET /v1/endorsers).

    Example sf-labor-council
  • endorser_statescomma-separated strings

    Endorsers’ home states (USPS codes), or national for endorsers with none.

    Example CA,national
  • statescomma-separated strings

    States the contests are voted in (USPS codes).

    Example CA
  • electionscomma-separated strings

    Election slugs (GET /v1/elections).

    Example ca-2026-11-03-general
  • contest_kindscomma-separated strings

    Contest kinds: candidate (an office), measure, or retention (whether to keep a judge).

    One of candidate, measure, retention.Example measure
  • levelscomma-separated strings

    Levels of the contest’s district, from federal down to school and special districts.

    One of federal, state, county, city, school, special.Example county,city
  • typescomma-separated strings

    Kinds of change: created (a new endorsement), updated (one that changed) or deleted (one withdrawn or removed). All three by default.

    One of created, updated, deleted.Example created,updated
  • cursorstring

    Resume 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.

    Example c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ
  • fromstring

    Where 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 of latest, earliest.Default latest.Example earliest
  • sincestring (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.

    Example 2026-10-27T00:00:00Z

Returns

200 An open event stream.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 404 Nothing has that id or slug.
  • 410 The cursor is older than the stream keeps.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
  • 503 Not ready yet on this server. Wait Retry-After seconds.
GET /v1/stream
curl -N "https://api.ballotdata.net/v1/stream?subscription=CA%20labor" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
With every parameter
# 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"
Response200text/event-stream
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

  • subscriptionstring

    A saved subscription’s id or name (GET /v1/subscriptions). Use it instead of the filter parameters below, not together with them.

    Example CA labor
  • categoriescomma-separated strings

    Endorser category slugs (GET /v1/endorser-categories).

    Example labor
  • endorserscomma-separated strings

    Endorser slugs (GET /v1/endorsers).

    Example sf-labor-council
  • endorser_statescomma-separated strings

    Endorsers’ home states (USPS codes), or national for endorsers with none.

    Example CA,national
  • statescomma-separated strings

    States the contests are voted in (USPS codes).

    Example CA
  • electionscomma-separated strings

    Election slugs (GET /v1/elections).

    Example ca-2026-11-03-general
  • contest_kindscomma-separated strings

    Contest kinds: candidate (an office), measure, or retention (whether to keep a judge).

    One of candidate, measure, retention.Example measure
  • levelscomma-separated strings

    Levels of the contest’s district, from federal down to school and special districts.

    One of federal, state, county, city, school, special.Example county,city
  • typescomma-separated strings

    Kinds of change: created (a new endorsement), updated (one that changed) or deleted (one withdrawn or removed). All three by default.

    One of created, updated, deleted.Example created,updated
  • cursorstring

    Resume 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.

    Example c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ
  • fromstring

    Where 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 of latest, earliest.Default latest.Example earliest
  • sincestring (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.

    Example 2026-10-27T00:00:00Z
  • limitinteger

    Events per page, 1–1000. has_more in the response says more are waiting.

    Default 100.1 to 1000.Example 500
  • waitinteger

    Seconds to hold the request open when nothing new matches (long polling), 0–25. 0 answers at once.

    Default 0.0 to 25.Example 20

Returns

200 Events and the next cursor.

  • dataarray of Event

    Changes, oldest first.

    10 fields
    • idstring

      The change’s id: increasing and unique. Use it to drop repeats.

    • typestring

      What happened.

      One of endorsement.created, endorsement.updated, endorsement.deleted.
    • occurred_atstring (date-time)

      When the change was made.

    • endorsementobject
      6 fields
      • idinteger

        Endorsement id.

      • rankinteger or null

        Ranked choice rank.

      • statementstring or null

        What the endorser said.

      • source_urlstring or null

        Where it was published.

      • published_onstring (date) or null

        When.

      • added_bystring

        How 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 of automatic, staff, data_load.
    • changedarray of string

      For endorsement.updated: the fields that changed. Empty for other events.

      One of choice_id, rank, statement, source_url, published_on.
    • previousobject or null

      For endorsement.updated: the values before the change. null for other events.

      5 fields
      • choice_idinteger

        The choice endorsed before.

      • rankinteger or null

        Rank before.

      • statementstring or null

        Statement before.

      • source_urlstring or null

        Source before.

      • published_onstring (date) or null

        Date before.

    • endorserobject
      5 fields
      • idinteger

        Endorser id.

      • slugstring or null

        Endorser slug.

      • namestring or null

        Endorser name.

      • categoryCategory or null

        Category.

        2 fields
        • slugstring

          Category slug, used in categories= filters.

        • namestring

          Display name.

      • statestring or null

        Home state, or null for national.

    • contestobject
      9 fields
      • idinteger

        Contest id.

      • titlestring or null

        Title.

      • short_titlestring or null

        Short title.

      • kindstring or null

        candidate, measure or retention.

      • vote_methodstring or null

        Vote method.

      • votes_allowedinteger or null

        Vote for N.

      • ranks_allowedinteger or null

        Ranks allowed.

      • measure_codestring or null

        Measure code.

      • districtobject or null

        District.

        5 fields
        • idinteger

          District id.

        • namestring

          Name.

        • levelstring

          Level.

        • ocd_idstring or null

          OCD id.

        • statestring or null

          State.

    • electionobject or null

      Election.

      6 fields
      • idinteger

        Election id.

      • slugstring

        Slug.

      • namestring

        Name.

      • datestring (date)

        Date.

      • statestring or null

        State.

      • typestring

        Type.

    • choiceobject
      7 fields
      • idinteger

        Choice id.

      • labelstring or null

        As printed.

      • measure_answerstring or null

        yes or no.

        One of yes, no.
      • candidateobject or null

        Candidate.

        2 fields
        • idinteger

          Candidate id.

        • full_namestring

          Full name.

      • partyobject or null

        Party.

        2 fields
        • slugstring

          Slug.

        • namestring

          Name.

      • withdrawnboolean or null

        Withdrawn.

      • incumbentboolean or null

        The candidate holds this seat now: true or false where we know, null where we don’t (and for a measure answer).

  • next_cursorstring

    Send as cursor= next time.

  • has_moreboolean

    More are waiting: call again now.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 404 Nothing has that id or slug.
  • 410 The cursor is older than the stream keeps.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
  • 503 Not ready yet on this server. Wait Retry-After seconds.
GET /v1/events
curl "https://api.ballotdata.net/v1/events?subscription=CA+labor&cursor=c1.eyJzIjp7IjAiOiIxODM0MSJ9fQ&wait=20" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
With every parameter
# 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"
Response200application/json
{
  "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 Subscription

    Every item.

    5 fields
    • idinteger

      Subscription id.

    • namestring

      Name, unique in your account.

    • filterFilter

      Which 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 string

        Endorser category slugs (GET /v1/endorser-categories).

      • endorsersarray of string

        Endorser slugs.

      • endorser_statesarray of string

        Endorsers’ home states (USPS codes), or "national".

      • statesarray of string

        States the contests are voted in.

      • electionsarray of string

        Election slugs.

      • contest_kindsarray of string

        Contest kinds.

        One of candidate, measure, retention.
      • levelsarray of string

        District levels.

        One of federal, state, county, city, school, special.
      • typesarray of string

        Stream only: kinds of change.

        One of created, updated, deleted.
    • created_atstring (date-time)

      Created.

    • updated_atstring (date-time)

      Last changed.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
GET /v1/subscriptions
curl "https://api.ballotdata.net/v1/subscriptions" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
Response200application/json
{
  "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.

  • namestring

    1–100 letters, digits, spaces, dots, dashes or underscores.

  • filterFilter

    Which 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 string

      Endorser category slugs (GET /v1/endorser-categories).

    • endorsersarray of string

      Endorser slugs.

    • endorser_statesarray of string

      Endorsers’ home states (USPS codes), or "national".

    • statesarray of string

      States the contests are voted in.

    • electionsarray of string

      Election slugs.

    • contest_kindsarray of string

      Contest kinds.

      One of candidate, measure, retention.
    • levelsarray of string

      District levels.

      One of federal, state, county, city, school, special.
    • typesarray of string

      Stream only: kinds of change.

      One of created, updated, deleted.

Returns

201 Created.

  • dataSubscription
    5 fields
    • idinteger

      Subscription id.

    • namestring

      Name, unique in your account.

    • filterFilter

      Which 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 string

        Endorser category slugs (GET /v1/endorser-categories).

      • endorsersarray of string

        Endorser slugs.

      • endorser_statesarray of string

        Endorsers’ home states (USPS codes), or "national".

      • statesarray of string

        States the contests are voted in.

      • electionsarray of string

        Election slugs.

      • contest_kindsarray of string

        Contest kinds.

        One of candidate, measure, retention.
      • levelsarray of string

        District levels.

        One of federal, state, county, city, school, special.
      • typesarray of string

        Stream only: kinds of change.

        One of created, updated, deleted.
    • created_atstring (date-time)

      Created.

    • updated_atstring (date-time)

      Last changed.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 409 A subscription with that name already exists.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
POST /v1/subscriptions
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"]}}'
Response201application/json
{
  "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

  • idintegerrequired

    Subscription id (from GET /v1/subscriptions).

    Example 7

Returns

200 The subscription.

  • dataSubscription
    5 fields
    • idinteger

      Subscription id.

    • namestring

      Name, unique in your account.

    • filterFilter

      Which 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 string

        Endorser category slugs (GET /v1/endorser-categories).

      • endorsersarray of string

        Endorser slugs.

      • endorser_statesarray of string

        Endorsers’ home states (USPS codes), or "national".

      • statesarray of string

        States the contests are voted in.

      • electionsarray of string

        Election slugs.

      • contest_kindsarray of string

        Contest kinds.

        One of candidate, measure, retention.
      • levelsarray of string

        District levels.

        One of federal, state, county, city, school, special.
      • typesarray of string

        Stream only: kinds of change.

        One of created, updated, deleted.
    • created_atstring (date-time)

      Created.

    • updated_atstring (date-time)

      Last changed.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 404 Nothing has that id or slug.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
GET /v1/subscriptions/{id}
curl "https://api.ballotdata.net/v1/subscriptions/7" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
Response200application/json
{
  "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

  • idintegerrequired

    Subscription id (from GET /v1/subscriptions).

    Example 7

Request body

JSON.

  • namestring

    1–100 letters, digits, spaces, dots, dashes or underscores.

  • filterFilter

    Which 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 string

      Endorser category slugs (GET /v1/endorser-categories).

    • endorsersarray of string

      Endorser slugs.

    • endorser_statesarray of string

      Endorsers’ home states (USPS codes), or "national".

    • statesarray of string

      States the contests are voted in.

    • electionsarray of string

      Election slugs.

    • contest_kindsarray of string

      Contest kinds.

      One of candidate, measure, retention.
    • levelsarray of string

      District levels.

      One of federal, state, county, city, school, special.
    • typesarray of string

      Stream only: kinds of change.

      One of created, updated, deleted.

Returns

200 Saved.

  • dataSubscription
    5 fields
    • idinteger

      Subscription id.

    • namestring

      Name, unique in your account.

    • filterFilter

      Which 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 string

        Endorser category slugs (GET /v1/endorser-categories).

      • endorsersarray of string

        Endorser slugs.

      • endorser_statesarray of string

        Endorsers’ home states (USPS codes), or "national".

      • statesarray of string

        States the contests are voted in.

      • electionsarray of string

        Election slugs.

      • contest_kindsarray of string

        Contest kinds.

        One of candidate, measure, retention.
      • levelsarray of string

        District levels.

        One of federal, state, county, city, school, special.
      • typesarray of string

        Stream only: kinds of change.

        One of created, updated, deleted.
    • created_atstring (date-time)

      Created.

    • updated_atstring (date-time)

      Last changed.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 404 Nothing has that id or slug.
  • 409 A subscription with that name already exists.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
PUT /v1/subscriptions/{id}
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"]}}'
Response200application/json
{
  "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

  • idintegerrequired

    Subscription id (from GET /v1/subscriptions).

    Example 7

Returns

204 Deleted. No body.

Errors

  • 400 A parameter is missing or not valid.
  • 401 No API key, or one that isn’t valid, was revoked or expired.
  • 403 The 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.
  • 404 Nothing has that id or slug.
  • 429 Your address sent too many requests with a missing or wrong API key. Wait Retry-After seconds. Requests with a working key are never limited.
DELETE /v1/subscriptions/{id}
curl -X DELETE "https://api.ballotdata.net/v1/subscriptions/7" \
  -H "Authorization: Bearer $BALLOTDATA_API_KEY"
Response204
(no body)
Stream API reference · BallotData API