> ## 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.

# Monitor corpus coverage and changes

> Poll changedSince for sections that moved since your last sync, and diff coverage snapshots to catch a jurisdiction gaining a whole new corpus

**What this gets you:** an early-warning system for your own product - know when the law you depend on moved, and when a state or agency you depend on gains coverage, instead of finding out by accident.

Two different questions live on this page, and they need two different tools.

| Question | Use |
| - | - |
| Did any section I care about change since my last run? | `changedSince` on `POST /us/statutes/search` |
| Did a jurisdiction gain a whole new corpus? | Snapshot and diff `GET /us/statutes/coverage` |
| Tell me the moment one specific section changes | [Law Change Alerts](/docs/api-guide/alerts), webhook or email, no polling |

## The sync filter: `changedSince`

`changedSince` takes a `YYYY-MM-DD` date and restricts a search to sections we have OBSERVED change on since then.
It is the purpose-built answer to "what moved since my last run", and it needs no snapshot files and no diffing of your own.

<CodeGroup>
  ```bash cURL theme={"theme":"github-dark"}
  curl -X POST https://api.vaquill.ai/api/v1/us/statutes/search \
    -H "Authorization: Bearer vq_key_..." \
    -H "Content-Type: application/json" \
    -d '{"query": "data breach notification", "corpusType": "STATE", "state": ["ca", "ny", "tx"], "changedSince": "2026-08-01", "limit": 50, "fields": ["title", "state", "lastAmendedDate", "actStatus"]}'
  ```

  ```python Python theme={"theme":"github-dark"}
  import requests

  BASE_URL = "https://api.vaquill.ai/api/v1/us"
  HEADERS = {
      "Authorization": "Bearer vq_key_...",
      "Content-Type": "application/json",
  }


  def changed_since(query: str, since: str, **scope) -> list[dict]:
      """Sections matching `query` that we observed change on or after `since`."""
      resp = requests.post(
          f"{BASE_URL}/statutes/search",
          headers=HEADERS,
          json={
              "query": query,
              "changedSince": since,
              "limit": 50,
              # Ask for only what your sync writes back. A result carries 40+
              # fields, most null on any row; actId and citation always come back.
              "fields": ["title", "state", "lastAmendedDate", "actStatus"],
              **scope,
          },
      )
      if resp.status_code == 422:
          # The window matched too many sections to answer safely. Narrow it.
          raise RuntimeError(resp.json())
      resp.raise_for_status()
      return resp.json()["results"]
  ```
</CodeGroup>

Then read each hit's history with `GET /us/statutes/section/{actId}/changes` when you need to know what actually moved.
`changedSince` and that endpoint are the same event stream, so a hit here always has a history there.

### Four things that decide whether your job is correct

* **These are observed changes, not effective dates.**
  A refresh compared the publisher against the copy we held and found it different.
  The date is when we SAW it, which is an upper bound on when it took effect.
* **Coverage is bounded by capture, not by the age of the law.**
  Change capture began long after the corpus did, it is per-source, and events are swept at 24 months.
  An empty result means no captured change in the window, never that nothing was amended.
* **A window that is too broad is refused, not truncated.**
  Match too many sections and the call returns `422` telling you to use a later date or a `state` filter.
  That is deliberate: a partial change set would look complete to a sync job, which would then mark itself current having never seen the sections that were cut.
  Narrow with `corpusType` and `state`.
* **A search still needs a `query`.**
  `changedSince` narrows a search, it is not a firehose of everything that moved.
  Run it once per topic you actually track, which is what makes the result small enough to act on.

<Note>
  Paging is bounded: `limit` caps at 50 and `offset` at 70, so the deepest
  reachable result is 120. If a window is returning full pages, tighten it with a
  later date, a `state` list, or a narrower `corpusType` rather than trying to
  page through it.
</Note>

<Note>
  An empty window is a real answer and is charged the ordinary 4-credit search
  price. "Nothing moved since Tuesday" is what lets a nightly job do nothing with
  confidence. Read `creditsConsumed` rather than assuming.
</Note>

### A nightly job

```python Python theme={"theme":"github-dark"}
from datetime import date, timedelta

TOPICS = [
    {"query": "data breach notification", "corpusType": "STATE", "state": ["ca", "ny", "tx"]},
    {"query": "hazard communication standard", "corpusType": "CFR"},
]


def run(since: str) -> None:
    for topic in TOPICS:
        scope = {k: v for k, v in topic.items() if k != "query"}
        query = topic["query"]
        hits = changed_since(query, since, **scope)
        if not hits:
            continue
        print(f"{len(hits)} change(s) touching {query!r} since {since}")
        for h in hits:
            print(f"  {h['citation']}  {h.get('lastAmendedDate') or ''}")


if __name__ == "__main__":
    # Overlap the window by a day so a run that fails cannot open a gap.
    run(since=(date.today() - timedelta(days=2)).isoformat())
```

## The other question: a jurisdiction gaining a new corpus

`changedSince` tells you a section moved.
It cannot tell you that a state you do not yet query has just gained regulations, because there was nothing there to change.
For that, snapshot `GET /us/statutes/coverage` on a schedule and diff the snapshots.
Coverage is free and rate limited, so daily or weekly is plenty.

<Steps>
  <Step title="Fetch and flatten the coverage matrix">
    The response is wrapped as `{ "data": {...}, "meta": {...} }`. Inside `data`
    you get a `corpusTypes` legend and a `jurisdictions` array, where each
    jurisdiction carries a `corpora` object mapping a corpusType token to its
    ingested section count (the same distinct-section count
    [Coverage](/docs/api-guide/coverage) shows, not the larger passage count used
    internally for retrieval).

    ```python Python theme={"theme":"github-dark"}
    def fetch_coverage() -> dict:
        resp = requests.get(
            f"{BASE_URL}/statutes/coverage",
            headers={"Authorization": HEADERS["Authorization"]},
        )
        resp.raise_for_status()
        return resp.json()["data"]


    def to_snapshot(coverage_data: dict) -> dict:
        """Flatten coverage into {code: {corpusToken: count}}."""
        return {
            j["code"]: dict(j["corpora"])
            for j in coverage_data["jurisdictions"]
            if j["hasData"]
        }
    ```

    A trimmed `data` payload:

    ```json theme={"theme":"github-dark"}
    {
      "corpusTypes": [
        {"token": "STATE", "description": "State statutory codes", "scope": "state"},
        {"token": "REGULATION", "description": "State administrative regulations", "scope": "state"}
      ],
      "jurisdictions": [
        {
          "code": "wa",
          "name": "Washington",
          "kind": "state",
          "sectionCount": 104795,
          "hasData": true,
          "corpora": {"STATE": 53595, "REGULATION": 51053, "STATE_CONSTITUTION": 147}
        }
      ],
      "totalSections": 3840955,
      "stateCountWithData": 50
    }
    ```
  </Step>

  <Step title="Persist and diff">
    Save each snapshot as JSON so the next run has a baseline, then compare the
    two maps for a new jurisdiction, a new corpus within a known jurisdiction, or
    an existing corpus whose count grew.

    ```python Python theme={"theme":"github-dark"}
    import json
    from datetime import datetime, timezone
    from pathlib import Path

    SNAPSHOT_DIR = Path("coverage_snapshots")


    def save_snapshot(snapshot: dict) -> Path:
        SNAPSHOT_DIR.mkdir(exist_ok=True)
        stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        path = SNAPSHOT_DIR / f"coverage-{stamp}.json"
        path.write_text(json.dumps(snapshot, indent=2, sort_keys=True))
        return path


    def load_latest_snapshot() -> dict | None:
        files = sorted(SNAPSHOT_DIR.glob("coverage-*.json"))
        return json.loads(files[-1].read_text()) if files else None


    def diff_snapshots(old: dict, new: dict) -> list[str]:
        changes: list[str] = []
        for code, corpora in new.items():
            if code not in old:
                changes.append(f"NEW jurisdiction {code}: {sorted(corpora)}")
                continue
            prev = old[code]
            for token, count in corpora.items():
                if token not in prev:
                    changes.append(f"{code}: new corpus {token} ({count} sections)")
                elif count > prev[token]:
                    changes.append(
                        f"{code}: {token} grew by {count - prev[token]} "
                        f"({prev[token]} -> {count})"
                    )
        return changes
    ```

    Example output on a run where a state gained regulations:

    ```text theme={"theme":"github-dark"}
    2 coverage change(s) detected:
      - va: new corpus REGULATION (22400 sections)
      - tx: STATE grew by 1840 (120967 -> 122807)
    ```
  </Step>
</Steps>

<Warning>
  A growing count is a coarse signal. It says the corpus got bigger, not which
  sections moved, and a re-ingest that replaces sections one for one leaves the
  count flat. Use the diff to decide that something happened, then use
  `changedSince` to find out what.
</Warning>

## Related

<CardGroup cols={2}>
  <Card title="Law Change Alerts" icon="bell" href="/docs/api-guide/alerts">
    Section-level change notifications with a webhook or email, no polling.
  </Card>

  <Card title="Compare a rule across states" icon="scale-balanced" href="/docs/api-guide/recipes/multi-state-comparison">
    Get the comparable provision in other states in one call.
  </Card>

  <Card title="Enrich a citation database" icon="database" href="/docs/api-guide/recipes/citation-enrichment">
    Attach canonical citations, excerpts, and source links to a batch of rows.
  </Card>

  <Card title="Coverage matrix" icon="table-cells" href="/docs/api-guide/coverage">
    The human-readable jurisdiction and corpus matrix.
  </Card>
</CardGroup>


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