curl --request POST \
--url https://api.vaquill.ai/api/v1/watches \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"corpusType": "state"
}
'import requests
url = "https://api.vaquill.ai/api/v1/watches"
payload = { "corpusType": "state" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({corpusType: 'state'})
};
fetch('https://api.vaquill.ai/api/v1/watches', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vaquill.ai/api/v1/watches"
payload := strings.NewReader("{\n \"corpusType\": \"state\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"data": {
"id": "9f2b1e0a-1234-4a11-8b1c-abcdef123456",
"corpusType": "state",
"state": "wa",
"channel": "webhook",
"webhookUrl": "https://example.com/hooks/law-changes",
"isActive": true,
"createdAt": "2026-08-07T09:00:00Z",
"updatedAt": "2026-08-07T09:00:00Z",
"webhookAuth": {
"scheme": "bearer",
"hasSecret": true
},
"changeSignal": "continuous"
},
"meta": {
"processingTimeMs": 45.2,
"creditsConsumed": 0
}
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": "Insufficient API credits."
}Create a board watch
Subscribe to a board. webhookUrl is validated against an SSRF blocklist (loopback/private/link-local/cloud-metadata ranges) before acceptance and again on every dispatch. Every board.updated delivery body includes a deliveryId (stable per watch per refresh event, even across our own internal retries) so you can dedup safely on your end.
Each item in the body’s changes[] carries the same id and detectedAt that GET /watches/{watchId}/changes returns, so a webhook item is directly usable: pass its id as sinceId to page the rest of a change set that overflowed changesOverflowCount, or to GET /watches/{watchId}/changes/{changeId}/diff for the before and after text.
The two sets of counters in that body measure different things over different populations, and they are not reconcilable against each other. Read sections*, and read rows* only if you already built on it:
sectionsAdded/sectionsAmended/sectionsRemovedcount SECTIONS, one per change event, which is the same unit aschanges[]and asGET /watches/{watchId}/changes. They are narrowed by this watch’sscope, which the body echoes back so you can see what they were counted over. They are omitted entirely when the refresh classified no change events, because there is then no honest section count to give.rowsAdded/rowsUpdated/rowsDeletedare the refresh’s raw storage counters for the WHOLE BOARD. They are never narrowed byscope, and they do not share a unit with each other: added and deleted count storage chunks (a long section is stored as several), while updated counts sections. One amended multi-section document therefore lands in all three at once, so a run that amended three sections can truthfully report 12 added, 3 updated and 12 removed.
Both can be true in the same body: a watch scoped to 21 CFR part 314 can receive rowsUpdated: 480 next to sectionsAmended: 1, because the first counts the eCFR refresh’s whole run and the second counts what happened inside your scope. The rows* keys are kept unchanged for receivers already built on them; do not present them to anyone as three populations of sections.
Free.
curl --request POST \
--url https://api.vaquill.ai/api/v1/watches \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"corpusType": "state"
}
'import requests
url = "https://api.vaquill.ai/api/v1/watches"
payload = { "corpusType": "state" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({corpusType: 'state'})
};
fetch('https://api.vaquill.ai/api/v1/watches', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.vaquill.ai/api/v1/watches"
payload := strings.NewReader("{\n \"corpusType\": \"state\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"data": {
"id": "9f2b1e0a-1234-4a11-8b1c-abcdef123456",
"corpusType": "state",
"state": "wa",
"channel": "webhook",
"webhookUrl": "https://example.com/hooks/law-changes",
"isActive": true,
"createdAt": "2026-08-07T09:00:00Z",
"updatedAt": "2026-08-07T09:00:00Z",
"webhookAuth": {
"scheme": "bearer",
"hasSecret": true
},
"changeSignal": "continuous"
},
"meta": {
"processingTimeMs": 45.2,
"creditsConsumed": 0
}
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": "Insufficient API credits."
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": "Insufficient API credits."
}Authorizations
API key issued from the developer dashboard. Pass as Authorization: Bearer vq_key_...
Body
Board's corpus_type (e.g. state, state_regulation, federal_register, agency_guidance). Matched case-insensitively, so the uppercase spelling the statutes endpoints use for the same body of law (USC, CFR) works here too. Call GET /boards for the authoritative, current list -- corpus_type is a growing set as new corpora are added, not a fixed enum, and not every corpus we serve is a watchable board.
"state"
Board's state (2-letter, case-insensitive). For a federal board (USC, eCFR, the Federal Register) pass federal or omit this entirely; the two are equivalent. Must otherwise match the state GET /boards returned for this corpusType -- GET /boards?corpusType=<type> lists exactly those.
federal, al, ak, az, ar, ca, co, ct, de, dc, fl, ga, hi, id, il, in, ia, ks, ky, la, me, md, ma, mi, mn, ms, mo, mt, ne, nv, nh, nj, nm, ny, nc, nd, oh, ok, or, pa, pr, ri, sc, sd, tn, tx, ut, vt, va, wa, wv, wi, wy "wa"
webhook, email, both Required when channel is webhook or both.
Optional signing secret, stored encrypted and never returned. When set, every delivery carries an X-Vaquill-Signature: sha256=<hex> header: HMAC-SHA256 of the raw request body bytes, keyed with this secret. To verify, compute the same HMAC over the raw body you received (before parsing JSON) and compare it, constant-time, to the hex digest after sha256=.
Required when channel is email or both.
Optional narrowing so this alert covers one citation instead of an entire source. Four mutually exclusive forms.
Hierarchy prefix -- keys title, chapter, part, section, e.g. {"title": "21", "part": "314"} for 21 CFR part 314. Every level you set must match, so each one narrows further. title is required whenever any narrower level is set, and section also needs chapter or part: a bare part number matches across unrelated titles. Only sources whose documents carry a title/chapter/part hierarchy accept this form; the rest return 400.
Exact section -- {"actId": "CFR_T21_P314_S314_50"}, using the actId returned by search results and change events. Case-sensitive, and validated at create time against the corpus: an act_id we do not hold, or one belonging to a different source, is a 400 rather than a watch that could never fire. This form works on EVERY source, including flat ones with no hierarchy, so it is the only way to follow a single Federal Register document.
Named source -- {"source": "fdic_fil"}, for the corpora that fold several independent bodies of law behind one corpusType. The agency_guidance board alone carries 47 named sources across 24 agencies, so an unscoped watch on it delivers FDIC letters, IRS notices, OFAC FAQs and DOE appliance standards together. Accepted on agency_adjudication, agency_guidance, agency_manuals, cfr, federal_rules; anything else returns 400. The pickable values for a given board are on that board's sources array in GET /boards -- deliberately narrower than the source filter published on POST /us/statutes/search, because agency_guidance and agency_manuals are two boards reading one corpusType and each emits only its own half.
Publishing agency -- {"agency": "irs"}, covering every source that agency issues on the board. source is the unit we ingest and it is finer than the unit most callers think in: the IRS alone publishes 5 named series, so following it by source means one watch per series and a gap if you miss one. An agency scope is expanded WHEN AN EVENT IS MATCHED rather than when the watch is created, so a source registered after your watch is covered by it automatically, with no action from you. Accepted on agency_adjudication, agency_guidance, agency_manuals; the pickable values are on that board's agencies array in GET /boards.
Omit entirely to watch the whole source, which is the default and the pre-existing behavior.
Show child attributes
Show child attributes
{ "part": "314", "title": "21" }
Optional outbound credential sent on every delivery, so your gateway can authenticate us with the header it already reads. Independent of webhookSecret: set neither, either, or both. Only valid on a webhook or both channel watch.
Show child attributes
Show child attributes
Response
The created watch.
Was this page helpful?

