---
title: "List what changed on a watch's source"
method: GET
path: "/api/v1/watches/{watch_id}/changes"
tags: ["Board Watches"]
---

# List what changed on a watch's source

`GET /api/v1/watches/{watch_id}/changes`

## Request

- Base URL: `https://api.vaquill.ai`
- URL: `https://api.vaquill.ai/api/v1/watches/{watch_id}/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/watches/:watch_id/changes/get.ts> (a function with typed inputs; its response is checked against the schema)

The sections this watch's source added, amended, or removed, newest first: the per-item detail behind the counts in a notification. It covers the source's whole captured history, not just the period since you subscribed; pass `since=` the watch's `createdAt` for what your alert has covered.

History runs from when change capture began for that source and is swept at 5 years. An empty list means no captured change, not that the law was never amended; the publisher's own history, where there is one, is on the section's `amendmentHistory`. The endpoint is read-only and safe to poll alongside a webhook or email watch: it cannot suppress, advance or double-fire a delivery.

Paging: pass `sinceId` with `meta.cursor` from your previous page to get only what is new, and keep going while `meta.hasMore` is true. With `sinceId` the order defaults to `asc` so the cursor never skips rows. To read newest first, pass `order=desc` and page older with `beforeId=meta.nextBeforeId`.

`changeKind` is `added` (a new section), `amended` (its content was replaced), or `removed` (it disappeared from a full refresh of the source). `citation` and `title` are null for corpora that do not carry them. `detectedAt` is when our refresh saw the change; `sourceChangedOn`, the publisher's own date, is carried on `cfr`, `federal_rules` and on no other board, and its absence never licenses reading `detectedAt` as the amendment date. Check `publishesSourceChangedOn` on `GET /boards`.

Free.

## Path parameters

- `watch_id` string, required: Id of the watch, as returned by `POST /watches` (`id`).

## Query parameters

- `limit` integer: Max changes to return.
- `sinceId` integer, nullable: Return only changes with an `id` greater than this. The cursor to build on: ids are a monotonic sequence, so this is exact, immune to clock skew, and cannot drop two changes that share a timestamp. Carry `meta.cursor` forward from your last page.
- `beforeId` integer, nullable: Return only changes with an `id` less than this: the cursor for walking BACK through history, where `sinceId` walks forward into new changes. A newest-first reader needs this one, since paging down a descending list means asking for what sits below the lowest id already held. Pass the smallest `id` on your last page.
- `since` string, nullable: Return only changes detected strictly after this ISO-8601 timestamp. Coarser than `sinceId` (a single refresh writes many rows at the same instant, and this drops all of them), but useful when all you kept was the `detectedAt` off a `changes[]` item in a webhook body. Prefer that item's `id` as `sinceId` where you still have it. Both may be combined.
- `changeKind` string[], nullable: Filter to these kinds. Repeat the parameter to pass several (`?changeKind=added&changeKind=amended`). Omit for all three.
- `order` 'asc' | 'desc', nullable: `desc` is newest first, for showing a recent-activity list, and is the default when `sinceId` is not set. When `sinceId` IS set the default is `asc`: catching up from a cursor walks forward, and an oldest-first page that hits `limit` leaves the unread gap at its far end, so `meta.cursor` can be carried forward without skipping rows. Pass `desc` together with `sinceId` explicitly to read the newest rows first; see `meta.cursor` and `meta.nextBeforeId`.

## Response `200`

What changed, newest first.

- ChangesEnvelope
  - `data` ChangesPage, required
    - `changes` ChangeResponse[], required
      - `id` integer, required
      - `refreshLogId` integer, nullable
      - `corpusType` string, required
      - `state` string, nullable
      - `changeKind` string, required
      - `actId` string, required
      - `citation` string, nullable
      - `displayCitation` string, nullable: Always-renderable citation. Falls back to the section hierarchy parsed from `actId` when `citation` was never captured, so a client never has to display an internal identifier. Prefer this for display; `citation` remains exactly what the capture path recorded.
      - `title` string, nullable
      - `detectedAt` string, required: When our refresh observed the change. This is a detection timestamp, not the date the law changed: a publisher can amend a section weeks before our next sweep reaches it. Use `sourceChangedOn` for the date at the source.
      - `sourceChangedOn` string, nullable: The date the publisher records the section as having changed (`YYYY-MM-DD`), e.g. eCFR's per-section `amendment_date`. Omitted, not null, when we do not have it, and its absence says nothing about the publisher. We carry this date on `cfr`, `federal_rules` and on no other board; `publishesSourceChangedOn` on `GET /boards` tells you which case you are in. Even there it is absent for sections the publisher gives no date for and for every `removed` section. Absent always means unknown. Do not fall back to `detectedAt`, which can trail the real amendment by months.
      - `lifecycleStage` string, nullable: Set only for a pending amendment to a Federal Rule that moved a stage (for example `adopted_by_supreme_court`). The rule's own text has not changed, so this change is not counted in `sectionsAmended`. Omitted for every ordinary change.
      - `relatedActId` string, nullable: Set with `lifecycleStage`: the `actId` of the proposed amendment. Fetch it for the proposed text. Omitted for every ordinary change.
      - `hasDiff` boolean
  - `meta` ResponseMeta, required: The `meta` block every board-watch route returns beside `data`.
    - `processingTimeMs` number, required
    - `creditsConsumed` number, required
    - `cursor` integer, nullable: Change feeds only: the value to send back as `sinceId`. Null on an empty page. The highest change `id` on the page, except on a full newest-first page read with a `sinceId`, where unread rows sit below the page and the cursor stays at your `sinceId`; use `nextBeforeId` to reach them.
    - `nextBeforeId` integer, nullable: Change feeds only, full `order=desc` pages: the lowest change `id` on the page. Send it as `beforeId` for the next older page. Null otherwise.
    - `hasMore` boolean, nullable: Change feeds only: true when the page filled to `limit`.

## Other responses

- `400`: Invalid changeKind, order, or since timestamp.
- `401`: Invalid or missing API key.
- `403`: API key lacks `research:read` scope.
- `404`: Watch not found.
- `422`: Validation error: `detail` plus one `errors` entry per rejected field.
- `429`: Rate limit exceeded.
- `500`: Internal error (`internal_error`, retryable). Any credits charged for the call are refunded; retry with backoff and quote `requestId` to support if it persists.

## Changes

- **2026-11-07** `340c0d24d0b6`: 3 breaking, 58 info
  - for the `query` request parameter `order`, default value `desc` was removed
  - removed the enum value `asc` from the `query` request parameter `order`
  - removed the enum value `desc` from the `query` request parameter `order`
  - `query` request parameter `order` list-of-types was widened by adding types `null`
  - …57 more

[Operation history](https://skmtc.dev/vaquill/apis/vaquill-developer-api/changes/api/v1/watches/:watch_id/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)
