Discovery

Eight endpoints for the questions that come before “fetch this bill”: how complete the data is, what changed since you last looked, what the corpus is about, and who is on the ballot.

All paths are relative to https://delilah-api.jsv21b.workers.dev. Everything here requires an X-API-Key header (Authentication) except /v1/coverage, which is public. Field names in the samples are exactly what the API returns; values written as <type> are placeholders.

Coverage

GET/v1/coverage

How complete each state is, as distinct from /v1/status, which answers how recent it is. Public — no API key — so you can measure the data before you integrate against it.

This endpoint takes no parameters. It is served from a snapshot refreshed out of band rather than computed per request, because the live aggregate over the whole corpus takes seconds against a sub-second budget; computed_at tells you how old the snapshot is. When the snapshot is unavailable it returns 503 coverage_unavailable.

bash
curl "https://delilah-api.jsv21b.workers.dev/v1/coverage"
json
{
  "generated_at": "<timestamp>",
  "computed_at": "<timestamp>",
  "states_total": "<int>",
  "states_in_session": "<int>",
  "totals": {
    "bills": "<int>",
    "votes": "<int>",
    "texts": "<int>",
    "sponsors": "<int>",
    "vs_reference": {
      "bills": "<float|null>",
      "votes": "<float|null>",
      "texts": "<float|null>"
    }
  },
  "per_state": [
    {
      "state": "<string>",
      "bills": "<int>",
      "pct_with_votes": "<int|null>",
      "pct_with_texts": "<int|null>",
      "pct_with_sponsors": "<int|null>",
      "votes_total": "<int>",
      "texts_total": "<int>",
      "sponsors_total": "<int>",
      "vs_reference": {
        "bills": "<float|null>",
        "votes": "<float|null>",
        "texts": "<float|null>"
      },
      "has_live_session": "<bool>"
    }
  ]
}

Read the ratios, not the percentages. Most bills die in committee without a roll call ever being taken, so a low pct_with_votes usually describes the legislature rather than a gap in the data — only vs_reference, which compares Delilah against the legacy reference provider, carries a judgment. vs_reference is null when the refresher could not reach the reference database, and the endpoint degrades to absolute coverage rather than disappearing.

Change feed

GET/v1/changes

The pull side of near-real-time: poll with a since cursor and get everything that moved after it. Without since the feed returns the last hour. Ordered newest first, with a summary object alongside data.

ParameterTypeRequiredDescription
sincestring (date-time)OptionalISO-8601 date or timestamp — your cursor. Defaults to one hour ago; non-ISO values return 400.
statestringOptional2-letter postal code (e.g. FL), DC, or US for Congress.
reasonstringOptionalExact reason name, e.g. StatusChange.
limitintegerOptionalMax rows. Defaults to 200, clamped to 1000.
bash
curl "https://delilah-api.jsv21b.workers.dev/v1/changes?state=FL&since=2026-08-12T00:00:00Z" \
  -H "X-API-Key: dk_live_…"
json
{
  "data": [
    {
      "bill_id": "<int>",
      "bill_number": "<string>",
      "state_abbr": "<string>",
      "title": "<string>",
      "reason_id": "<int>",
      "reason_name": "<string>",
      "changed_at": "<timestamp>"
    }
  ],
  "summary": {
    "since": "<timestamp>",
    "server_now": "<timestamp>"
  }
}

Reason types include NewBill, StatusChange, Text, Vote, and Amendment, among others. Advance your cursor using summary.server_nowrather than your own clock — that keeps the window anchored to the API’s time, so clock skew on your side cannot silently drop events.

Topics

GET/v1/topics

The canonical topic taxonomy: roughly forty polished buckets that fold tens of thousands of raw state subject labels into one cross-state vocabulary, each with a bill count. This is what a topic picker should be built on. Takes no parameters.

bash
curl "https://delilah-api.jsv21b.workers.dev/v1/topics" \
  -H "X-API-Key: dk_live_…"
json
{
  "data": [
    {
      "slug": "<string>",
      "display_name": "<string>",
      "description": "<string>",
      "emoji": "<string>",
      "sort_order": "<int>",
      "bill_count": "<int>"
    }
  ]
}

A slug from here is what /v1/bills?topic= expects. See also the Topics guide.

Topic detail

GET/v1/topics/{slug}

One topic, plus a sample of the raw subject names that map into it — the introspection call for “what does Healthcare actually contain in this data”. Unlike most endpoints, the topic object is returned at the top level rather than nested under data.

ParameterTypeRequiredDescription
slugstring (path)RequiredTopic slug — lowercase letters, digits, and dashes, e.g. healthcare. A malformed slug returns 400; an unknown one returns 404 not_found.
bash
curl "https://delilah-api.jsv21b.workers.dev/v1/topics/healthcare" \
  -H "X-API-Key: dk_live_…"
json
{
  "slug": "<string>",
  "display_name": "<string>",
  "description": "<string>",
  "emoji": "<string>",
  "sort_order": "<int>",
  "bill_count": "<int>",
  "raw_subjects_mapped": "<int>",
  "sample_subjects": ["<string>"]
}

raw_subjects_mapped counts every raw subject folded into the topic; sample_subjects shows up to 25 of them, ordered by how many bills each carries.

Subjects

GET/v1/subjects

The raw subject firehose underneath the taxonomy — each state’s own labels with a bill count, ordered by count. Use this when you need the legislature’s exact vocabulary rather than the cross-state buckets.

ParameterTypeRequiredDescription
statestringOptional2-letter postal code (e.g. FL), DC, or US for Congress.
qstringOptionalSubstring match on the subject name.
limitintegerOptionalMax rows. Defaults to 100, clamped to 1000.
bash
curl "https://delilah-api.jsv21b.workers.dev/v1/subjects?state=FL&q=insurance" \
  -H "X-API-Key: dk_live_…"
json
{
  "data": [
    {
      "subject_id": "<int>",
      "subject_name": "<string>",
      "bill_count": "<int>"
    }
  ],
  "count": "<int>"
}

State portals tag bills with interface labels as well as real subjects — “Bill Text”, “First Reading”, “My Bills”. Those are filtered out before the response is built, which is why count can be lower than the limit you asked for.

Event stream (SSE)

GET/v1/stream

The push side of near-real-time: a Server-Sent Events stream of the same change rows /v1/changes returns. Responses are text/event-stream, not JSON.

ParameterTypeRequiredDescription
statesstringOptionalCSV of 2–3 letter codes, e.g. FL,CA. Anything else returns 400.
sincestring (date-time)OptionalISO-8601 resume cursor. Defaults to 60 seconds ago; the Last-Event-ID header takes precedence over it.
reasonstringOptionalExact reason name, e.g. Vote.
bash
curl -N "https://delilah-api.jsv21b.workers.dev/v1/stream?states=FL,CA" \
  -H "X-API-Key: dk_live_…"
text
retry: 5000
: delilah-stream v1

id: <timestamp>
event: bill_change
data: {"bill_id":"<int>","bill_number":"<string>","state":"<string>","title":"<string>","reason":"<string>","reason_id":"<int>","changed_at":"<timestamp>"}

: ping <timestamp>

event: rotate
data: {"reason":"wall_clock","cursor":"<timestamp>"}

Each connection lives about 85 seconds and then sends a rotate event carrying the cursor it stopped at. A standard EventSource client handles this on its own: it reconnects after the advertised retry interval and sends the last id back as Last-Event-ID, so the stream resumes exactly where it left off. Comment lines beginning with : are heartbeats every 15 seconds that keep intermediate proxies from killing an idle stream — clients ignore them by spec. A database failure mid-stream arrives as an error event rather than a dropped connection.

Note the payload keys differ slightly from the pull feed: the stream sends state and reason where /v1/changes sends state_abbr and reason_name.

Candidates

GET/v1/candidates

Candidate profiles from the nightly FEC and Ballotpedia sync — the bridge between who is legislating now and who is running next. Ordered by name, with a pagination object.

ParameterTypeRequiredDescription
statestringOptionalTwo-letter code. Anything else returns 400.
officestringOptionalSubstring match on the FEC office.
cycleintegerOptionalFour-digit election year. Anything else returns 400.
fec_idstringOptionalExact FEC candidate id.
bioguidestringOptionalExact Bioguide id.
limitintegerOptionalMax rows. Defaults to 50, clamped to 500.
offsetintegerOptionalRow offset. Defaults to 0.
bash
curl "https://delilah-api.jsv21b.workers.dev/v1/candidates?state=FL&cycle=2026" \
  -H "X-API-Key: dk_live_…"
json
{
  "data": [
    {
      "id": "<int>",
      "name": "<string>",
      "party": "<string>",
      "photo_url": "<string>",
      "people_id": "<int|null>",
      "fec_id": "<string|null>",
      "office": "<string|null>",
      "state": "<string|null>",
      "district": "<string|null>",
      "cycle": "<int|null>",
      "incumbent_challenge": "<string|null>",
      "bioguide_id": "<string|null>"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "returned": 50 }
}

people_id is the crosswalk, and it is deterministic — no name matching. Where the pipeline linked one, it keys straight into /v1/legislators/{people_id} and its voting and sponsorship drill-downs, so a candidate can be shown next to their actual record. Challengers carry FEC and Ballotpedia data only, which is why the state, office, and cycle filters match only candidates with an FEC record.

Candidate profile

GET/v1/candidates/{id}

One candidate in full — bio, website, socials, photos, the FEC side, and every election candidacy attached to them, newest election first. Returns 404 not_found for an unknown id.

ParameterTypeRequiredDescription
idinteger, int64 (path)RequiredCandidate identifier. Non-integer values return 400.
bash
curl "https://delilah-api.jsv21b.workers.dev/v1/candidates/12345" \
  -H "X-API-Key: dk_live_…"
json
{
  "data": {
    "id": "<int>",
    "name": "<string>",
    "party": "<string>",
    "bio": "<string>",
    "website": "<string>",
    "socials": "<json>",
    "photo_url": "<string>",
    "bp_photo_url": "<string>",
    "bp_url": "<string>",
    "people_id": "<int|null>",
    "fec_id": "<string|null>",
    "office": "<string|null>",
    "state": "<string|null>",
    "district": "<string|null>",
    "cycle": "<int|null>",
    "incumbent_challenge": "<string|null>",
    "bioguide_id": "<string|null>",
    "candidacies": [
      {
        "contest_id": "<int>",
        "party": "<string>",
        "is_incumbent": "<bool>",
        "withdrew": "<bool>",
        "votes": "<int>",
        "vote_pct": "<float>",
        "is_winner": "<bool>",
        "header": "<string>",
        "election_date": "<date>",
        "office_name": "<string>",
        "office_level": "<string>",
        "chamber": "<string>",
        "district": "<string>",
        "election_year": "<int>",
        "race_state": "<string>"
      }
    ]
  }
}

socials is an object rather than a string. Inside candidacies, district and race_state describe the race being contested, which is not necessarily the same as the state and district on the candidate record itself.