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
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.
curl "https://delilah-api.jsv21b.workers.dev/v1/coverage"
{
"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
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
since | string (date-time) | Optional | ISO-8601 date or timestamp — your cursor. Defaults to one hour ago; non-ISO values return 400. |
state | string | Optional | 2-letter postal code (e.g. FL), DC, or US for Congress. |
reason | string | Optional | Exact reason name, e.g. StatusChange. |
limit | integer | Optional | Max rows. Defaults to 200, clamped to 1000. |
curl "https://delilah-api.jsv21b.workers.dev/v1/changes?state=FL&since=2026-08-12T00:00:00Z" \ -H "X-API-Key: dk_live_…"
{
"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
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.
curl "https://delilah-api.jsv21b.workers.dev/v1/topics" \ -H "X-API-Key: dk_live_…"
{
"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
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string (path) | Required | Topic slug — lowercase letters, digits, and dashes, e.g. healthcare. A malformed slug returns 400; an unknown one returns 404 not_found. |
curl "https://delilah-api.jsv21b.workers.dev/v1/topics/healthcare" \ -H "X-API-Key: dk_live_…"
{
"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
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
state | string | Optional | 2-letter postal code (e.g. FL), DC, or US for Congress. |
q | string | Optional | Substring match on the subject name. |
limit | integer | Optional | Max rows. Defaults to 100, clamped to 1000. |
curl "https://delilah-api.jsv21b.workers.dev/v1/subjects?state=FL&q=insurance" \ -H "X-API-Key: dk_live_…"
{
"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)
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
states | string | Optional | CSV of 2–3 letter codes, e.g. FL,CA. Anything else returns 400. |
since | string (date-time) | Optional | ISO-8601 resume cursor. Defaults to 60 seconds ago; the Last-Event-ID header takes precedence over it. |
reason | string | Optional | Exact reason name, e.g. Vote. |
curl -N "https://delilah-api.jsv21b.workers.dev/v1/stream?states=FL,CA" \ -H "X-API-Key: dk_live_…"
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
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
state | string | Optional | Two-letter code. Anything else returns 400. |
office | string | Optional | Substring match on the FEC office. |
cycle | integer | Optional | Four-digit election year. Anything else returns 400. |
fec_id | string | Optional | Exact FEC candidate id. |
bioguide | string | Optional | Exact Bioguide id. |
limit | integer | Optional | Max rows. Defaults to 50, clamped to 500. |
offset | integer | Optional | Row offset. Defaults to 0. |
curl "https://delilah-api.jsv21b.workers.dev/v1/candidates?state=FL&cycle=2026" \ -H "X-API-Key: dk_live_…"
{
"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
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer, int64 (path) | Required | Candidate identifier. Non-integer values return 400. |
curl "https://delilah-api.jsv21b.workers.dev/v1/candidates/12345" \ -H "X-API-Key: dk_live_…"
{
"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.

