Skip to main content
A board is a tracked source, identified by 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: true when POST /watches accepts a scope for that source. It is false for sources such as the Federal Register, where an item is a numbered document with no title or part.
  • scopeBrowseCorpus: the corpusType to pass to GET /us/statutes/divisions to list the source’s titles, chapters and parts. Its spelling can differ from the board’s (a board is cfr, and /us/statutes/divisions wants CFR). When it is null, narrowing still works but no endpoint lists the hierarchy.
cURL
Python
limit defaults to 500, which is also the maximum. If the list grows past 500, a call without offset returns the first 500 and no error. Read data.total and data.hasMore, and page on offset.
You can also filter with 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, or both. webhookUrl is required for webhook and both. emailAddress is required for email and 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 must be a public address. Loopback, private, link-local and cloud metadata addresses are rejected.
  • corpusType: a value GET /boards returns. Send the board key (state_regulation) or the search-style token (REGULATION becomes state_regulation, USC becomes usc). Case does not matter. Any other value is a 422 that 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_register and federal_bill.
  • 404 if the corpusType and state pair does not match a board. 409 if you already have a watch on that board, channel and scope. 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:
  • title is required when you set any narrower level.
  • section also needs chapter or part.
Values can be strings or numbers and are stored lowercased. "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’s STATE_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 scopable is false. It is the only way to follow a single Federal Register document or one Justice Manual chapter.
A renumbered section gets a new act_id, so the watch stops matching. The watch fires once with a removed event for the old act_id.

Authenticating our deliveries

Two independent options are available. Set neither, either, or both.
cURL
The credential is stored encrypted and never returned. GET /watches shows only {"scheme": "...", "headerName": "...", "hasSecret": true}. Send webhookAuth as a whole object when you change it:
  • 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.
  • 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 a POST 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
A watch on the 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, pass sinceId with the meta.cursor from your previous page. Ids always increase, so this never skips or repeats a change.
cURL
Python

Paging backwards with beforeId

To backfill history from before you created the watch, keep order=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
A good pattern is to use the webhook for speed and poll on a slow timer to catch deliveries your endpoint missed. Deduplicate on the change 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

When hasDiff 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/changes feed.
Last modified on November 11, 2026