---
title: "Feed of changes to the session-law registry"
method: GET
path: "/api/v1/us/session-laws/changes"
tags: ["US Session Laws"]
---

# Feed of changes to the session-law registry

`GET /api/v1/us/session-laws/changes`

## Request

- Base URL: `https://api.vaquill.ai`
- URL: `https://api.vaquill.ai/api/v1/us/session-laws/changes`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-Key`
- TypeScript: <https://skmtc.dev/vaquill/apis/vaquill-developer-api/docs/api/v1/us/session-laws/changes/get.ts> (a function with typed inputs; its response is checked against the schema)

A sync feed of the registry: every state law that began to be served, had its text added, was
corrected, unserved, withdrawn or merged, oldest first, as a cursor over an integer `id`. Poll it to
keep a copy of the registry current without re-listing it: read from `sinceId=0` once, then call
again with the `cursor` you were given. It covers state session laws only; federal acts (`SAL_...`)
are not in it.

**Cost**: 1 credit per page of up to 200 entries (default 50). An empty page is
an answer, "nothing changed since your cursor", and is charged. A request that fails validation
(422) is not charged; a failure on our side (500, 503) is refunded.

**Walking the feed.** Send `sinceId=0` for the first page. While `hasMore` is true, call again with
`sinceId` set to `cursor`: `id` only grows and a page carries the entries after `sinceId` and
nothing else, so no entry is skipped or repeated between your calls. When `hasMore` is false you
have caught up as of this call; keep your cursor and ask again later. An empty page has
`cursor: null`: keep the one you hold. `id` is assigned in order, but a row can become visible
slightly after a later one if two writers commit at once (writers commit whole batches together, so
this has not happened); a job that cannot miss an entry can re-read the last few hundred ids.

**What an entry says.** `kind` is what happened (see its description), `sessionLawId` the law,
`jurisdiction` its state, and `detectedAt` when we recorded it: an upper bound on when the law
changed in the world, not an approval or effective date. Read the law at
`GET /us/session-laws/{sessionLawId}`, or many at once with `POST /us/session-laws/batch`. A
`withdrawn` or `merged` entry is a tombstone: the law no longer answers there, and a `merged` entry
carries `mergedInto`, the `sessionLawId` to read instead.

**Filter.** `jurisdiction` limits the feed to one state, `dc` or `pr`, and then `hasMore` means more
matching entries follow. The response echoes the applied filter as `jurisdiction` (null when
unfiltered); an empty `jurisdiction=` is a 422, not "every state". The cursor is a position in the
whole feed, so the same `sinceId` means the same point with or without the filter, and a filtered
page can skip ids.

**Example**

```bash
curl "https://api.vaquill.ai/api/v1/us/session-laws/changes?sinceId=0&limit=100" \
  -H "Authorization: Bearer $VAQUILL_API_KEY"
```

```javascript
let cursor = 0;
for (;;) {
  const res = await fetch(`https://api.vaquill.ai/api/v1/us/session-laws/changes?sinceId=${cursor}`, {
    headers: { Authorization: `Bearer ${process.env.VAQUILL_API_KEY}` },
  });
  const page = await res.json();
  for (const change of page.changes) handle(change);
  if (page.cursor !== null) cursor = page.cursor;
  if (!page.hasMore) break;
}
```

**Related endpoints**

- `GET /us/session-laws/list` lists the laws of one state, filtered; this feed is how you learn
  that one changed.
- `POST /us/session-laws/batch` reads many laws at once from the ids this feed returns.
- `GET /us/statutes/coverage` says which sessions we hold and how complete each is, in each
  jurisdiction's `sessionLaws` block.

**Nulls.** Every response field is present: one with no value is `null` (`[]` for a list). The
examples here leave the nulls out.

## Query parameters

- `sinceId` integer: Return only entries with an `id` greater than this: the cursor for walking the feed forward. Send 0 (the default) to start from the beginning, then the `cursor` of your last page. `id` is an integer that only grows, so this is exact and immune to clock skew. A negative or non-integer value is a 422 and is never charged; a value past the newest `id` is an empty page, which is charged.
- `jurisdiction` 'al' | 'ak' | 'as' | 'az' | 'ar' | 'ca' | 'co' | 'ct' | 'de' | 'dc' | 'fl' | 'ga' | 'gu' | 'hi' | 'id' | 'il' | 'in' | 'ia' | 'ks' | 'ky' | 'la' | 'me' | 'md' | 'ma' | 'mi' | 'mn' | 'ms' | 'mo' | 'mt' | 'ne' | 'nv' | 'nh' | 'nj' | 'nm' | 'ny' | 'nc' | 'nd' | 'mp' | 'oh' | 'ok' | 'or' | 'pa' | 'pr' | 'ri' | 'sc' | 'sd' | 'tn' | 'tx' | 'ut' | 'vt' | 'vi' | 'va' | 'wa' | 'wv' | 'wi' | 'wy', nullable: Limit the feed to one jurisdiction: a two-letter state code, `dc` or `pr`, case-insensitive; the response echoes it as `jurisdiction`. Omit for every jurisdiction (an empty value is a 422, not "every state"). `federal` and any unknown code is a 422 and is never charged: federal session laws are not in this feed. The cursor is a position in the whole feed, so a filtered page may have gaps in `id`.
- `limit` integer: Most entries on this page: 1 to 200, default 50. The price is per page, however many entries it holds, so a larger page is cheaper per entry. Out of range is a 422 and is never charged.

## Headers

- `Idempotency-Key` string, nullable: Optional. 16 to 255 visible ASCII characters (a UUIDv4 works). Repeating the same request with the same key replays the original 2xx response for 24 hours without charging again. The same key on a different request is rejected. With the account's `cacheQueryDerivedData` retention setting off, no body is stored and a retry answers 409 `idempotency_replay_unavailable`, still without a second charge.

## Response `200`

A page of the feed, oldest first. `hasMore` and `cursor` say how to continue; an empty `changes` is a charged answer.

- SessionLawChangesResponse: Response for `GET /us/session-laws/changes`.
  - `changes` SessionLawChange[]: The entries after `sinceId`, oldest first, at most `limit`. Empty when nothing changed since your cursor, which is an answer and is charged.
    - `id` integer, required: The entry's position in the feed: an integer that only ever grows. Send the largest `id` you hold as `sinceId` to read what came after it.
    - `kind` 'added' | 'text_available' | 'corrected' | 'served' | 'unserved' | 'withdrawn' | 'merged', required: What happened to the law. `added`: the registry learned the act exists. `text_available`: its text was collected. `corrected`: our reading of the law (its identity, dates or text) was corrected, so a copy you hold may be stale. `served`: it began to be served by this API. `unserved`: it stopped being served. `withdrawn`: it was removed from the registry, a tombstone: `GET /us/session-laws/{sessionLawId}` no longer answers it. `merged`: it was found to be the same law as another and merged into it, also a tombstone. So far every entry is `served`, one per law served when the registry went live; the other kinds appear as laws change. A consumer should treat a kind it does not know as `corrected`: re-read the law.
    - `sessionLawId` string, required: The law this entry is about. Read it at `GET /us/session-laws/{sessionLawId}`, except after `withdrawn` or `merged`, when it no longer answers.
    - `jurisdiction` string, required: Lowercase two-letter code of the state, `dc` or `pr`.
    - `mergedInto` string, nullable: On a `merged` entry, the `sessionLawId` of the law this one was merged into: read that one instead. Null for every other kind, and for a `merged` entry recorded before the target was exposed (read the law's own id to follow it).
    - `detectedAt` string, required: When we recorded the change, ISO 8601 in UTC: the start of the transaction that wrote it. An upper bound on when the law changed in the world, never the date the law took effect or was approved.
  - `count` integer: Entries on this page.
  - `jurisdiction` string, nullable: The `jurisdiction` filter that was applied, lowercase, or null when the feed was not filtered (every state).
  - `cursor` integer, nullable: The largest `id` on this page: send it as `sinceId` for the next page. Null when the page is empty, so keep the cursor you already hold.
  - `hasMore` boolean: True when more entries follow this page: call again with `sinceId` set to `cursor` straight away. False means you have caught up as of this call.
  - `creditsConsumed` integer: Credits actually charged for this call, never the list price. 0 when the call was refunded (a failure on our side).
  - `processingTimeMs` number: Server-side time for this request in milliseconds, excluding network transit. Not billed on.

## Other responses

- `401`: Missing, invalid or revoked API key. Not charged.
- `402`: Insufficient credits for this call. Nothing was done and nothing was charged. Read your balance, free, at `GET /credits/balance`.
- `403`: The API key lacks the `research:read` scope. Not charged.
- `409`: Idempotency conflict. `idempotency_in_progress`: the original request with this key is still running (retry after `Retry-After`). `idempotency_replay_unavailable`: it completed but its response was too large to replay; nothing is charged again, so check the credit balance instead of retrying.
- `422`: A parameter failed validation: `sinceId` negative or not an integer, `limit` out of range, or `jurisdiction` not a state. Never charged. `errors[].loc` names the parameter exactly as you send it. `idempotency_key_reused`: the `Idempotency-Key` was already used with a different request.
- `429`: Rate limit exceeded for your key. Not charged. Wait `Retry-After` seconds.
- `500`: An unexpected failure on our side. The charge is refunded.
- `503`: The session-laws database is temporarily unavailable. The charge is refunded; retry.

## Changes

- **2026-11-07** `340c0d24d0b6`: 74 info
  - added the optional property `candidateCount` to the response with the `401` status
  - added the optional property `candidateCount` to the response with the `402` status
  - added the optional property `candidateCount` to the response with the `403` status
  - added the optional property `candidateCount` to the response with the `409` status
  - …70 more

[Operation history](https://skmtc.dev/vaquill/apis/vaquill-developer-api/changes/api/v1/us/session-laws/changes/get.md)

---

[API](https://skmtc.dev/vaquill/apis/vaquill-developer-api.md) - [All operations](https://skmtc.dev/vaquill/apis/vaquill-developer-api/llms.txt) - [OpenAPI document](https://skmtc.dev/vaquill/apis/vaquill-developer-api/revisions/340c0d24d0b6?raw)
