corpusType and state. Subscribe to one and we notify you by webhook or email when it changes.
Every alert names the sections that changed. For an amended section you can fetch the old and new text.
Start with GET /boards, then POST /watches.
Every path below is prefixed with /api/v1 and every request needs an Authorization: Bearer vq_key_... header.
Free routes still count against your rate limit.
Only the diff is metered, because it rebuilds both sides of the text.
Two plan limits apply to watches:
Going over the watch cap returns 429. Delete a watch before creating another, or upgrade.
Boards and watches
GET /boards lists every board you can watch. Each has a corpusType, an optional state (omitted for federal boards), a label, and a refresh cadence.
Compare lastRetrievedAt against cadence to see how recent a source is.
For the cadence of each source, see Which statute cadence applies where.
GET /boards lists only what can be watched, which is narrower than what we cover. For coverage, use GET /us/statutes/coverage.
A watch is your subscription to one board, on a webhook, email, or both. By default it covers the whole board. To follow one title or part, see Narrow a watch to a citation.
Two fields on each board show whether narrowing is available:
scopable:truewhenPOST /watchesaccepts ascopefor that source. It isfalsefor sources such as the Federal Register, where an item is a numbered document with no title or part.scopeBrowseCorpus: thecorpusTypeto pass toGET /us/statutes/divisionsto list the source’s titles, chapters and parts. Its spelling can differ from the board’s (a board iscfr, and/us/statutes/divisionswantsCFR). When it isnull, narrowing still works but no endpoint lists the hierarchy.
cURL
Python pager for all boards
Python pager for all boards
Python
corpusType and state. Pass state=federal for boards with no state (USC, eCFR, the Federal Register). Omitting state is not the same.
Create a watch
POST /watches with the board’s corpusType (and state, for a state board) plus a channel.
channel:webhook,email, orboth.webhookUrlis required forwebhookandboth.emailAddressis required foremailandboth.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.webhookUrlmust be a public address. Loopback, private, link-local and cloud metadata addresses are rejected.corpusType: a valueGET /boardsreturns. Send the board key (state_regulation) or the search-style token (REGULATIONbecomesstate_regulation,USCbecomesusc). Case does not matter. Any other value is a422that lists the allowed values. These cannot be watched:constitution,executive_action,agency_action,cfr_annual,usc_annual,federal_rules_annual,federal_rules_proposed,us_treaty,state_session_law,state_registerandfederal_bill.404if thecorpusTypeandstatepair does not match a board.409if you already have a watch on that board, channel andscope. Different scopes on the same board and channel are separate watches, so you can follow several parts at once.
GET /watches lists your 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
A watch on a whole source reports every change. The federal regulations change daily, so to follow only 21 CFR part 314, set a scope:scope takes four optional keys, from broadest to narrowest: title, chapter, part, section. Omit scope to watch the whole source.
Two rules apply, and both return a 400 if broken:
titleis required when you set any narrower level.sectionalso needschapterorpart.
"221.2" and "221_2" match the same section. Read scope from the response to see what we stored.
A scoped watch stays silent when nothing in its scope changed. Deliveries carry a scope field, and changes holds only what is inside it. GET /watches/{watchId}/changes applies the same filter. Email subjects name the scope, for example eCFR (title 21, part 314) was updated.
Sending a hierarchy scope to a source where scopable is false returns a 400 that lists the sources that accept one.
Or watch one exact section
Some sources have act_ids with no title, such as Missouri’sSTATE_MO_C407_S407.822. A hierarchy scope cannot narrow these, because it requires title.
Instead, name one section with the actId that search results and change events return:
- It cannot be combined with the hierarchy keys. Sending both is a
400. - It is case-sensitive. Copy it verbatim (
STATE_LA_Crevised-statutes_T14_S112.11). - It is checked when you create the watch. An act_id we do not hold, or one from a different source, is a
400. - It works on every source, including those where
scopableisfalse. It is the only way to follow a single Federal Register document or one Justice Manual chapter.
Authenticating our deliveries
Two independent options are available. Set neither, either, or both.cURL
GET /watches shows only {"scheme": "...", "headerName": "...", "hasSecret": true}.
Send webhookAuth as a whole object when you change it:
- Change the credential: same
scheme, newsecret. - Rename the header, keep the token: same
scheme, newheaderName, and omitsecret. - Change scheme: send
secretagain. - Turn it off:
{"scheme": "none"}, which also deletes the stored credential.
headerName cannot be Authorization (use bearer or basic), a transport header, or an X-Vaquill-* header. These return a 400.
Receiving a webhook delivery
A delivery is aPOST to your webhookUrl:
changes holds at most 10 items. changesOverflowCount is the count beyond that. changeKind is added, amended, or removed. citation and title are null for some corpora, so fall back to actId.
deliveryId is stable per watch per refresh event, so you can use it to deduplicate.
Verifying the signature
X-Vaquill-Signature is an HMAC of the request body. It is sent only when you set webhookSecret.
Compute HMAC-SHA256(webhookSecret, raw_request_body_bytes) and compare it, in constant time, to the hex digest after sha256=. Use the raw bytes you received, before parsing JSON. Re-serializing the parsed object changes key order and the digest will not match.
Python
email or both channel also gets an email naming the same sections.
Test before you rely on it
POST /watches/{watchId}/test sends a one-off delivery with X-Vaquill-Event: board.test. It is not saved to delivery history. You can send 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 the sections a board added, amended, or removed, newest first. It includes changes from before you subscribed. To see only changes since you subscribed, pass since set to the watch’s createdAt.
Events are kept for about five years. An empty list means we captured no change, not that a section was never amended.
Polling
You can poll this endpoint as often as you like, including alongside a webhook or email watch. Polling never affects deliveries. To fetch only what is new, passsinceId with the meta.cursor from your previous page. Ids always increase, so this never skips or repeats a change.
cURL
Python poller
Python poller
Python
Paging backwards with beforeId
To backfill history from before you created the watch, keeporder=desc and pass the lowest id you have seen as beforeId. Repeat until a page comes back empty.
meta.cursor is the highest id on the page, so take the lowest id from the page yourself.
Python backfill
Python backfill
Python
id, or on deliveryId for webhook payloads.
A change can be retracted. Its id then stops appearing in the feed and its diff returns 404. Treat an id you hold that no longer appears as withdrawn. Ids are never reused.
Read the diff for one change
WhenhasDiff is true, call GET /watches/{watchId}/changes/{changeId}/diff for the section’s text before and after that change. It costs 4 credits, charged whether or not both sides are available, so check hasDiff first.
hasDiff (on the change) and hasBefore and hasAfter (on the diff) can be false. An added change has nothing to compare against, and corpusType=cfr never reports a diff. Every other corpus reports a diff for every amended change.
Pending Federal Rules amendments
A watch on a Federal Rule (corpusType=federal_rules) also fires when a pending amendment to that rule moves a stage.
The rule’s own text has not changed on that day. An event looks like this:
lifecycleStage and relatedActId appear only on these events, in the change feed, in changes[] of a board.updated webhook, and on a section’s change history.
A lifecycle event is not counted in sectionsAmended.
FEDERAL_RULES_ANNUAL and FEDERAL_RULES_PROPOSED have no boards of their own. Watch the rule.
See Federal coverage.
See also
- Monitor corpus coverage: detect when a jurisdiction gains a new corpus.
- Errors: how error responses are shaped, and what each status code means.
- Coverage: the jurisdiction and corpus matrix you are choosing a board from.
- SDKs: the official SDKs create watches and verify webhook signatures for you.
- State session laws: the session-law registry has its own
GET /us/session-laws/changesfeed.

