> ## Documentation Index
> Fetch the complete documentation index at: https://vaquill.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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` / `sectionsRemoved` count SECTIONS, one per change event, which is the same unit as `changes[]` and as `GET /watches/{watchId}/changes`. They are narrowed by this watch's `scope`, 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` / `rowsDeleted` are the refresh's raw storage counters for the WHOLE BOARD. They are never narrowed by `scope`, 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.**



## OpenAPI

````yaml https://api.vaquill.ai/external/openapi.json post /api/v1/watches
openapi: 3.1.0
info:
  title: Vaquill Developer API
  description: >-
    Public API for legal statutes and legislation.


    **Authentication**: Pass your API key as `Authorization: Bearer vq_key_...`
    (preferred) or in the `X-API-Key` header. The `api_key` query parameter also
    works where a header cannot be set, but a URL ends up in logs and browser
    history. Sending two different keys in one request is rejected with a 401.


    **Credits**: Each call costs API credits. See `GET
    /api/v1/api-credits/pricing` for the full pricing matrix.


    ## Jurisdictions


    | Jurisdiction | Coverage | `countryCode` |

    |---|---|---|

    | **United States** | USC and the Statutes at Large, the CFR and its annual
    editions, all 52 state and territory statutory codes (50 states plus DC and
    Puerto Rico), state administrative regulations, court rules, federal and
    state constitutions, federal agency guidance and adjudications, the Federal
    Register, the Sentencing Guidelines and US tax treaties. | `US` |


    See the full [Coverage page](https://www.vaquill.ai/docs/api-guide/coverage)
    for the jurisdiction-by-jurisdiction breakdown, or call
    `/us/statutes/coverage` for live counts. Coverage is per (jurisdiction,
    corpusType) pair, so treat that endpoint as the authoritative list rather
    than this summary.


    ## Endpoints


    - **US Statutes**: Search and retrieve any of the corpora above with full
    text, resolve a citation to its exact section, browse the statutory
    hierarchy, read a section as it stood on a past date, and follow its change
    history, cross-references and defined terms

    - **Board Watches**: Subscribe to a corpus source and get a webhook or email
    when it refreshes with real changes (free, API key required)

    - **Pricing**: Credit costs per endpoint (no auth required)


    ## Rate limits


    Limits are enforced per API key and scale with your plan. A `429` response
    includes a `Retry-After` header.


    | Plan | Per minute | Per hour | Per day |

    |---|---|---|---|

    | Vaquill API Pro | 60 | 1,000 | 2,000 |

    | Vaquill API Business | 150 | 2,500 | 10,000 |


    For full documentation, visit the [API
    Reference](https://www.vaquill.ai/docs/api-reference/).
  version: 1.0.0
  contact:
    name: Vaquill API Support
    url: https://www.vaquill.ai
    email: support@vaquill.ai
  license:
    name: Proprietary
    url: https://www.vaquill.ai/terms
  termsOfService: https://www.vaquill.ai/terms
servers:
  - url: https://api.vaquill.ai
    description: Production
security:
  - ApiKeyAuth: []
  - ApiKeyHeader: []
  - ApiKeyQuery: []
tags:
  - name: US Statutes
    description: >-
      Search and retrieve US primary law. Covers the United States Code and the
      Statutes at Large, the Code of Federal Regulations and its annual
      editions, state statutory codes and administrative regulations, federal
      and state constitutions, court rules, Federal Register rules and
      Presidential documents, federal and state agency guidance, agency
      adjudications, state Attorney General opinions, the Sentencing Guidelines
      and US tax treaties. Scope a search with the `corpusType` filter, and call
      `/us/statutes/coverage` for the live jurisdiction-by-corpusType list.


      - **Search**: Search across all USC, CFR, state, and regulatory sections

      - **Section**: Get metadata, citation, hierarchy, and source links for a
      specific section

      - **Body**: Get the full text of a section

      - **Enactments**: Which state session laws a publisher's own tables list
      against a section (supported jurisdictions only, never a complete history)
  - name: US Session Laws
    description: >-
      State session laws: the acts of each legislature as they were enacted,
      chapter by chapter, before they are folded into a code. A session law is
      what the governor signs. It carries the act's own text, including the
      language it strikes and inserts, its approval and effective dates, and,
      where the publisher prints one, the code sections it changes. That makes
      it the source for "which act changed this section, and when did it take
      effect".


      Every act has a permanent `sessionLawId` (for example
      `SSL_MN_2025R_G_Y2025_C1`, Minnesota Laws 2025, regular session, chapter
      1) whether or not we hold its text, and a citation or bill number such as
      `S.F.No. 1552` is accepted where an id goes. v1 serves STATE acts only.
      Federal session laws (the Statutes at Large, `SAL_` ids) are served by
      `/us/statutes/section`, and `/us/statutes/resolve` points a session-law
      citation here.


      **Start here**: check `/us/statutes/coverage` (free) for what we hold per
      state: each jurisdiction's `sessionLaws` block lists its sessions, which
      are complete or only partly collected, and the `series` and
      `instrumentTypes` that exist. Then **List** a session or a date range to
      get ids, **Get** one act for its record, then **Body** for its text. A law
      missing from a partly collected session is a free 404
      (`session_not_fully_collected`), not proof it does not exist.


      - **List**: everything enacted in a state's session, approved in a window,
      or taking effect on a date

      - **Get**: one act by id or by citation; dates (`effectiveDates` is always
      a list), sources and the code sections it affects

      - **Body**: the act's text as printed, paged by cursor, with amendatory
      markup preserved where the publisher printed it

      - **Batch**: up to 50 ids or citations in one call, the same record as
      Get, billed per law served, with every miss listed and its reason

      - **Changes**: a sync feed of the registry over an integer cursor
      (`sinceId`): what began to be served, was corrected, withdrawn or merged


      The reverse link, from a code section to the acts a publisher's own tables
      list against it, is `GET /us/statutes/section/{actId}/enactments`.
  - name: Board Watches
    description: >-
      Law-change alerts. Subscribe to a **board** (a tracked corpus source,
      identified by `corpusType` plus an optional `state`) and get a webhook or
      email whenever it refreshes with real changes, naming the exact sections
      that changed. Fetch old-vs-new text for any amended section via the change
      diff.


      Boards refresh on their own existing cadence; watching one does not change
      how often it refreshes. Every endpoint here is **free**: authenticated by
      API key and rate-limited, but not credit-metered.


      See the [Law Change Alerts
      guide](https://www.vaquill.ai/docs/api-guide/alerts) for the full
      walkthrough.
  - name: Pricing
    description: >-
      Credit pricing and conversion rates. No authentication required. 1 credit
      = $0.01 USD.
  - name: Credits
    description: >-
      Your own credit balance: what is spendable right now, where it came from,
      and what is about to expire.


      Free and never charged, so it is safe to poll for low-balance alerting or
      to pre-flight a batch job.
externalDocs:
  description: Full API Reference
  url: https://www.vaquill.ai/docs/api-reference/
paths:
  /api/v1/watches:
    post:
      tags:
        - Board Watches
      summary: Create a board watch
      description: >-
        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` / `sectionsRemoved` count
        SECTIONS, one per change event, which is the same unit as `changes[]`
        and as `GET /watches/{watchId}/changes`. They are narrowed by this
        watch's `scope`, 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` / `rowsDeleted` are the refresh's raw
        storage counters for the WHOLE BOARD. They are never narrowed by
        `scope`, 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.**
      operationId: create_watch_api_v1_watches_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWatchRequest'
      responses:
        '201':
          description: The created watch.
          content:
            application/json:
              schema: {}
              example:
                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
        '400':
          description: >-
            Invalid request (bad channel/URL combo, rejected webhook target, or
            an invalid or unsupported scope).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
        '401':
          description: Invalid or missing API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
        '403':
          description: >-
            API key lacks `research:read` scope, or the plan does not include
            webhook delivery (Business only; email delivery is available on
            every plan).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
        '404':
          description: No enabled board for that corpusType + state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
        '409':
          description: You already have a watch on this board for this channel.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '429':
          description: >-
            Rate limit exceeded, or you have reached the maximum number of
            watches your plan allows (3 outside the Business plan).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiDetailError'
components:
  schemas:
    CreateWatchRequest:
      properties:
        corpusType:
          type: string
          title: Corpustype
          description: >-
            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.
          examples:
            - state
        state:
          anyOf:
            - type: string
              enum:
                - federal
                - al
                - ak
                - as
                - az
                - ar
                - ca
                - co
                - ct
                - de
                - dc
                - fl
                - ga
                - gu
                - hi
                - id
                - il
                - in
                - ia
                - ks
                - ky
                - la
                - me
                - md
                - ma
                - mi
                - mn
                - ms
                - mo
                - mt
                - ne
                - nv
                - nh
                - nj
                - nm
                - ny
                - nc
                - nd
                - mp
                - oh
                - ok
                - or
                - pa
                - pr
                - ri
                - sc
                - sd
                - tn
                - tx
                - ut
                - vt
                - vi
                - va
                - wa
                - wv
                - wi
                - wy
            - type: 'null'
          title: State
          description: >-
            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.
          examples:
            - wa
        channel:
          type: string
          enum:
            - webhook
            - email
            - both
          title: Channel
          default: webhook
        webhookUrl:
          anyOf:
            - type: string
            - type: 'null'
          title: Webhookurl
          description: Required when channel is webhook or both.
        webhookSecret:
          anyOf:
            - type: string
            - type: 'null'
          title: Webhooksecret
          description: >-
            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=`.
        emailAddress:
          anyOf:
            - type: string
            - type: 'null'
          title: Emailaddress
          description: Required when channel is email or both.
        scope:
          anyOf:
            - additionalProperties:
                type: string
              type: object
            - type: 'null'
          title: Scope
          description: >-
            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.
          examples:
            - part: '314'
              title: '21'
        webhookAuth:
          anyOf:
            - $ref: '#/components/schemas/WebhookAuthRequest'
            - type: 'null'
          description: >-
            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.
      type: object
      required:
        - corpusType
      title: CreateWatchRequest
    ApiDetailError:
      properties:
        detail:
          type: string
          title: Detail
          description: >-
            Human-readable reason, safe to surface to an end user. Branch on the
            HTTP status rather than on this string: the wording is not part of
            the contract and may be reworded, but 401 (bad key), 402 (out of
            credits), 403 (missing scope), 404 (no such resource) and 429 (rate
            limited) are stable.
          examples:
            - Insufficient API credits.
        errors:
          anyOf:
            - items:
                $ref: '#/components/schemas/ApiFieldError'
              type: array
            - type: 'null'
          title: Errors
          description: 'Present on 422 only: every field that failed validation.'
      type: object
      required:
        - detail
      title: ApiDetailError
      description: >-
        Error envelope the API actually returns.


        Every error (400/401/402/403/404/405/422/429/5xx) carries a `detail`

        string, e.g. `{"detail": "Insufficient API credits."}`, on every mount
        of

        the API. A 422 adds `errors`, one entry per rejected field. Some 404s
        add

        endpoint-specific diagnosis beside `detail` (the statutes section routes

        add `actId`, `reason` and `didYouMean`); treat unknown keys as optional.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    WebhookAuthRequest:
      properties:
        scheme:
          type: string
          enum:
            - none
            - bearer
            - basic
            - header
          title: Scheme
          description: >-
            `bearer` sends `Authorization: Bearer <secret>`. `basic` sends
            `Authorization: Basic <secret>` with the secret already
            base64-encoded by you. `header` sends `<headerName>: <secret>`, for
            gateways that read something like `X-Api-Key`. `none` removes any
            credential currently stored.
          default: none
        secret:
          anyOf:
            - type: string
            - type: 'null'
          title: Secret
          description: >-
            The credential value. Stored encrypted and never returned. Required
            when setting a scheme for the first time or changing scheme; on
            PATCH you may omit it to keep the stored one while changing only
            `headerName`.
        headerName:
          anyOf:
            - type: string
            - type: 'null'
          title: Headername
          description: >-
            Header to send the credential on. Required for `header`, and
            rejected for every other scheme. Cannot be `Authorization` (use
            `bearer`/`basic`), a transport header, or one of ours.
          examples:
            - X-Api-Key
      type: object
      title: WebhookAuthRequest
      description: |-
        How a delivery should authenticate itself to your endpoint.

        SEPARATE FROM `webhookSecret`, and the distinction is the point.
        `webhookSecret` signs the body so you can prove it is intact and ours.
        This sends a credential so your gateway can reject anything else before
        it reaches your handler. Most integrations want the second, many want
        both, and the two are set independently.
    ApiFieldError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Loc
          description: >-
            Where the bad value is: `body`, `query` or `path`, then the
            parameter name exactly as you send it (camelCase), then list
            indexes. A bare `["body"]` means the body as a whole was not a JSON
            object.
          examples:
            - - body
              - corpusType
        msg:
          type: string
          title: Msg
          description: What is wrong with the value, in plain English.
          examples:
            - Input should be 'USC', 'CFR' or 'STATE'
        type:
          type: string
          title: Type
          description: >-
            Machine-readable error kind, e.g. `missing`, `literal_error`,
            `json_invalid`.
          examples:
            - literal_error
      type: object
      required:
        - loc
        - msg
        - type
      title: ApiFieldError
      description: One rejected field in a 422 response.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: vq_key_*
      description: >-
        API key issued from the developer dashboard. Pass as `Authorization:
        Bearer vq_key_...` (preferred).
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        The same API key as a bare header value: `X-API-Key: vq_key_...`.
        Equivalent to the Bearer form.
    ApiKeyQuery:
      type: apiKey
      in: query
      name: api_key
      description: >-
        The same API key as a query parameter: `?api_key=vq_key_...`. Use only
        where you cannot set a header. A URL can end up in proxy and server
        logs, browser history and shared links, so prefer either header form,
        and rotate a key that has leaked.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.