API docs

Client libraries

Our libraries for JavaScript (and TypeScript) and Python are both called ballotdata. They cover every endpoint in the API reference and the Stream API reference, and handle the details for you: retries after 429 and 503 with Retry-After, timeouts, paging and typed errors, and an Endorsement Stream that reconnects and resumes where it stopped. Plain HTTP works just as well; every request in the references has a curl sample too.

LanguagePackageRuns onDocumentation
JavaScript, TypeScriptballotdata on npmNode 20+, Deno, Bun, Cloudflare Workers. No dependencies.README on GitHub
Pythonballotdata on PyPIPython 3.10+, sync and asyncio. Needs only httpx.README on PyPI

Install

Install
npm install ballotdata

Make a client

Create a key in the console (your API account opens when you buy at ballotdata.net) and put it in the BALLOTDATA_API_KEY environment variable. The client reads it from there, or you can pass it in. Keep keys on servers, never in a browser page or a mobile app.

Make a client
import { BallotData } from 'ballotdata';

const client = new BallotData({
  apiKey: process.env.BALLOTDATA_API_KEY, // the default, so you can leave it out
  timeout: 30_000,                        // ms to wait for each response
  maxRetries: 2,                          // retries for 429, 503 and network errors
});

Your first request

Methods are named after the endpoints (client.endorsements.list() is GET /v1/endorsements, client.contests.get(id) is GET /v1/contests/{id}) and take the same parameters. Responses have the API’s field names, so the reference reads the same as your code. List parameters take an array or a comma-separated string.

Endorsements
// What labor unions endorse on California's ballot
const { data } = await client.endorsements.list({ states: 'CA', categories: 'labor' });

// Every endorser's pick on one race, with the race's choices
const contest = await client.contests.get(4410, { categories: ['labor', 'editorial'] });
for (const { endorser, picks } of contest.endorsements) {
  console.log(endorser.name, picks.map((p) => (p.rank ? `${p.rank}. ${p.label}` : p.label)).join(', '));
}

Paging

A list method gives you the first page. Loop over it to get every item on every page, fetched as you go. To page by hand, pass the last page’s next_cursor as cursor.

Paging
// One page: { data, next_cursor }
const page = await client.endorsements.list({ states: 'CA', limit: 500 });

// Every item, across every page
for await (const endorsement of client.endorsements.list({ states: 'CA', limit: 500 })) {
  await save(endorsement);
}

The Endorsement StreamReturning for 2028

client.stream.connect() opens GET /v1/stream and yields every endorsement change that matches your filter or saved subscription. It reconnects by itself after the hourly reconnect, a dropped connection or a silent one, and calls you with each new cursor to save. Where a connection can’t stay open, client.events.follow() polls /v1/events instead. Both need a key with stream:read; see Live stream.

Endorsement Stream
const stream = client.stream.connect(
  { states: ['CA', 'NV'], categories: 'labor', cursor: await loadCursor() },
  { onCursor: (cursor) => saveCursor(cursor) },
);
for await (const message of stream) {
  if (message.type === 'endorsement') {
    const { type, endorser, choice, contest } = message.event; // endorsement.created | updated | deleted
    console.log(type, endorser.name, choice.label, contest.title);
  }
}

Postbacks can be checked with verifyPostbackSignature() or verify_postback_signature(); see Signed postbacks.

Errors

Every error the API answers with is raised as an error with its status, code (such as outside_purchase), message and request id, with a class for each kind: NotFoundError, RateLimitError, CursorExpiredError and so on. The codes are listed in Keys and errors.

Errors
import { ApiError, NotFoundError } from 'ballotdata';

let contest;
try {
  contest = await client.contests.get(4410);
} catch (err) {
  if (!(err instanceof NotFoundError)) {
    if (err instanceof ApiError) console.error(err.status, err.code, err.message, err.requestId);
    throw err;
  }
  contest = null;
}
Client libraries · BallotData API