Skip to main content
Corpus data updates on its own refresh cadence (daily for the Federal Register and the eCFR, weekly for the US Code and agency guidance, monthly for state codes, regulations, and court rules). Law Change Alerts let you subscribe to a board (a tracked source, identified by corpusType + state) and get notified automatically whenever it refreshes with real changes, instead of polling on your own schedule. Every alert names the exact sections that changed, not just a count, and you can fetch the old text next to the new text for any section that was amended. Every path below is prefixed with /api/v1 and every request needs an Authorization: Bearer vq_key_... header. All endpoints in this guide are free: authenticated and rate-limited, but not credit-metered.

Boards and watches

A board is a source you can watch: GET /boards lists every one, each with a corpusType, an optional state (omitted for federal boards), a human label, and its refresh cadence. Watching a board does not change how often it refreshes. A watch is your subscription to one board, on one or more channels (webhook, email, or both). By default a watch covers the whole board; see Narrow a watch to a citation to follow one title or part instead. Two fields on each board tell you whether narrowing is available. scopable is true when POST /watches accepts a scope for that source. It is false for sources whose documents are not citations, such as the Federal Register, where an item is a numbered document rather than a section of a code, so there is no title or part to narrow by. scopeBrowseCorpus is the corpusType to pass to GET /statutes/divisions if you want to enumerate a source’s titles, chapters and parts. Note that it is deliberately a different vocabulary from the board’s own corpusType: a board is cfr, and /statutes/divisions wants CFR. When scopable is true and scopeBrowseCorpus is null, narrowing works but there is no endpoint that lists that source’s hierarchy for you.

Create a watch

POST /watches with the board’s corpusType (and state, for a state board) plus a channel.
  • channel: webhook, email, or both. webhookUrl is required for webhook/both; emailAddress is required for email/both.
  • webhookSecret (optional): stored encrypted, never returned. When set, every delivery is HMAC-signed (see below).
  • webhookAuth (optional): a credential we send on every delivery, so your gateway can authenticate us. See Authenticating our deliveries.
  • webhookUrl is validated against an SSRF blocklist (loopback, private, and link-local ranges, plus cloud metadata endpoints) at creation and again on every dispatch.
  • 404 if the corpusType/state pair does not match a real, enabled board. 409 if you already have a watch on that board, for that channel, with that same scope. Two different scopes on the same board and channel are two separate watches, which is how you follow several parts at once.
GET /watches lists your own watches. PATCH /watches/{watchId} pauses (isActive: false), resumes, or rotates a destination without losing history. DELETE /watches/{watchId} removes it.

Narrow a watch to a citation

Watching an entire source can be more than you want. The Federal regulations refresh daily and change constantly, and a watch on all of cfr will tell you about every one of them. If you only care about 21 CFR part 314, say so:
scope takes four optional keys, from broadest to narrowest: title, chapter, part, section. Omit scope entirely to watch the whole source, which is the default and what every watch created before this feature does. Two rules keep a scope from matching more than you meant. title is required whenever you set any narrower level, because a bare part number appears in many unrelated titles. section also needs chapter or part. Both are rejected with a 400 naming the problem. Values may be sent as strings or numbers, and are stored lowercased. A section written with a dot and one written with an underscore are the same section, so "221.2" and "221_2" both match. The watch echoes back exactly what we stored, so read scope from the response rather than assuming your input was kept verbatim.
A scoped watch is silent when nothing inside its scope changed. It does not send an empty “the source was updated” notification on every refresh that touched some other part of the corpus, because that is the noise you narrowed the watch to avoid. Silence means nothing in your scope moved.
Everything else follows the scope too. Deliveries carry a scope field so a receiver can tell which of its watches fired, changes contains only what is inside it, and GET /watches/{watchId}/changes applies the same filter, so the read-back never disagrees with what we sent you. Email subjects name the scope, so eCFR (title 21, part 314) was updated rather than the same line for every alert you hold on that source. Sending a scope to a source where scopable is false returns a 400 listing the sources that do accept one.

Authenticating our deliveries

There are two independent mechanisms, and choosing between them starts with noticing they answer different questions. Most teams reach for webhookAuth first, because it works with middleware they already have. Set neither, either, or both.
cURL
The credential is stored encrypted and never returned. GET /watches shows only {"scheme": "...", "headerName": "...", "hasSecret": true}, so you can confirm what is configured without us handing the secret back.
Rotating and removing. webhookAuth is sent as a whole object rather than field by field, because a scheme without a credential is a broken config, not a partial edit.
  • Change the credential: same scheme, new secret.
  • Rename the header, keep the token: same scheme, new headerName, and omit secret.
  • Change scheme: send secret again. We will not reuse a bearer token as a raw header value, because that is a guess about your intent that would fail at your endpoint with nothing explaining why.
  • Turn it off: {"scheme": "none"}, which also deletes the stored credential.
headerName cannot be Authorization (use bearer or basic), a transport header, or one of ours (X-Vaquill-*). Those are rejected at registration with a 400 rather than failing silently at delivery time.

Receiving a webhook delivery

A delivery is a POST to your webhookUrl:
changes is capped at 10 items; changesOverflowCount is the true count beyond that. changeKind is added, amended, or removed. citation/title are null for corpora that don’t carry them, so fall back to actId.
deliveryId is stable per watch per refresh event, even across our own internal retries, so you can dedup safely on your end.

Verifying the signature

X-Vaquill-Signature is a real HMAC, not a token echoed back. It is present only when you set webhookSecret; if you want a static credential your gateway can check instead, that is webhookAuth. Compute HMAC-SHA256(webhookSecret, raw_request_body_bytes) and compare it, constant-time, to the hex digest after sha256=. Compute it over the raw bytes you received, before parsing JSON: re-serializing the parsed object changes key order and whitespace, and the digest will not match.
Python
An email/both channel watch gets an email instead (or in addition), naming the same changed sections.

Test before you rely on it

POST /watches/{watchId}/test fires a one-off delivery, sent with X-Vaquill-Event: board.test so your handler can tell it apart from a real update. Not persisted to delivery history, and does not affect the watch’s real last-notified state. Limited to one test per watch every 30 seconds.
cURL
GET /watches/{watchId}/deliveries returns webhook delivery history (attempt number, status code, success, error), newest first. Email-only watches always return an empty list here.

What changed, and the diff

GET /watches/{watchId}/changes lists exactly which sections a board added, amended, or removed, newest first, scoped to the watch’s own lifetime (a change from before you subscribed never appears, matching your delivery history).

Polling

You can poll this endpoint as often as you like, including alongside a webhook or email watch.
There is no “last checked” state to interfere with. Notifications fire when a corpus refresh completes, not by comparing against a stored watermark, and this endpoint writes nothing. Polling cannot suppress, advance, or double-fire a delivery. (lastNotifiedAt on a watch records the outcome of the last delivery attempt, for your visibility. It is never an input to what gets sent.)
To fetch only what is new, pass sinceId with the meta.cursor from your previous page. Ids are a monotonic sequence, so this is exact: it cannot skip a change or return one twice, and it is immune to clock skew.
meta.hasMore is true when the page filled to limit, so keep calling with the new cursor until it is false. Use order=asc while catching up: walking forward means a page that hits limit leaves the gap at a known end.
Running both channels is a reasonable design: take the webhook as the low-latency path, and poll on a slow timer as a backstop for deliveries your endpoint missed while it was down. Dedup on the change id, or on deliveryId for whole webhook payloads.
When hasDiff is true, call GET /watches/{watchId}/changes/{changeId}/diff for the section’s text before and after that specific change:
hasDiff (on the change) and hasBefore/hasAfter (on the diff) can legitimately be false: an added change has nothing to diff against, and a handful of sources track only that a section changed, not the text itself (see below). Check the flags rather than assuming both sides are always present.
A small number of sources (corpusType=cfr, and any change from before this feature shipped) report real added/amended/removed events but never a diff — hasDiff stays false for them by design, not by bug. Everything else, including every state corpus, reports a real diff for every amended change.

Monitor corpus coverage

A DIY alternative for detecting when a jurisdiction gains an entirely new corpus, rather than a section-level change within one you already watch.

Rate limits

Per-plan limits and how to back off on a 429.

Errors

How error responses are shaped, and what each status code means.

Coverage

The jurisdiction and corpus matrix you’re choosing a board from.
Last modified on August 12, 2026