A legal data API should guarantee three things about its response shape: which fields are present on every row no matter what you queried, which fields belong only to certain corpora, and what each field actually means when it is populated. Most integrations skip that question. They call the endpoint once, read whatever keys came back in the sample response, and write code against that shape. Then a new corpus ships, a field that was always populated on federal rows turns up null on a state row, and the parser throws at 2am. The fix is not better error handling. It is a stated contract you can hold the vendor to.
TL;DR
- A field list is not a contract. The contract is the tier each field sits in: always present, corpus-specific, or populated only when the publisher printed the underlying fact.
- Most fields are null on any given row, and that is correct. A comment close date means something for a Federal Register proposed rule and nothing for a state constitution.
- Vaquill AI's search result object publishes 128 fields, and exactly one of them (
actId) is schema-required. That asymmetry is the whole design. - Three API behaviors make a contract enforceable: unknown
fieldsnames return 422, unknown filter values return 422, and paging is cut from a single ranking so rows never repeat or vanish between pages. - Ask your vendor which fields are guaranteed, in writing. If the answer is "look at the sample response," you are integrating against a snapshot, not an interface.
- No US primary law API we know of offers point-in-time retrieval by date. What exists is amendment history, a last-amended-year filter, and change events. Anyone promising
as_of=1998-04-01should be asked to demo it.

How many fields does the Vaquill AI statute search result object publish?
Part of our legal data infrastructure series, which covers how this corpus is sourced, refreshed, and served.
For related coverage, see 128 Fields on One Statute: What Rich Legal Metadata Actually Buys You for the field-by-field inventory behind the tiers below, Legal Data Provenance: How to Tell Where a Statute Actually Came From, and Amendment History and Point-in-Time Law: What a Legal API Can and Cannot Tell You.
Why integrations break
Here is the failure, in order, because it is always the same order.
A developer queries a legal API for a US Code section. The response comes back with a title number, a chapter name, an effective date, and a link to the official text. They map those four fields into their own model and ship.
Six weeks later the product expands to state law. The state row has no chapter name (that state's code uses articles), no effective date (the publisher prints an amendment credit instead), and a source link that points to a state portal rather than a federal one. Nothing in the API changed. The developer's assumption about the API changed nothing either, because it was never true.
That is the gap a schema contract closes. It is not a longer field list. It is a statement about which fields are load-bearing and which are optional by design.
The six tiers
Every field in a legal primary-law response, and every field in the response field reference, falls into one of six tiers. Group them this way and the integration becomes obvious: tier 1 you can index on, tier 2 you can render, tiers 3 through 6 you check before you use.
Tier 1: always present
Two fields, and only two.
| Field | Type | Guarantee |
|---|---|---|
actId | string | The only schema-required field. Stable identifier for one citable section. |
citation | string | Always retained even when the response is pruned, because a row without a citation is unattributable. |
Everything else is optional in the schema sense. That sounds thin until you notice what it buys you: your primary key and your attribution string are the two things that can never be missing, so a row is always storable and always citable.
The fields parameter proves it. If you ask for fields: ["title", "excerpt"], the API still returns actId and citation on top of what you asked for. They are not droppable.
One trap worth stating plainly. actId is stable but not guessable. A real one looks like USC_T42_C21_S1983 or SAL_PL116-136_DVA_TII_S1109 (that second one is section 1109 of the CARES Act). Hand-built ids usually 404. Take the id from a search or a citation-resolve response and store it.
Tier 2: identity and hierarchy
These answer "where does this section sit in the code": citationShort, title and sectionTitle, corpusType, state, source, the container levels (titleNumber, chapter, part, subpart, each with a name variant), sectionNumber, displayPath, breadcrumb, and parent. That many levels sounds excessive until you normalize across 53 jurisdictions, and the field-by-field walk through all nine families covers what each one carries.
The contract point is the population rule rather than the list. titleNumber is alphanumeric where the publisher uses that (Alabama has a title 13A). source is null for corpora with a single source. And parent, the field most people miss, hands you a ready-made GET /us/statutes/divisions query for this section's siblings, so you never reconstruct a hierarchy query from string parts. It is null when the container cannot be determined, because an executive order does not sit inside a chapter.
Tier 3: provenance
This is the tier an enterprise buyer should read first, because it is the one that decides whether an answer is checkable. It carries the publisher's own page (externalUrl), the format mirrors a given publisher offers (stateHtmlUrl, govInfoHtmlUrl, govInfoPdfUrl), publisherKey, and the offsets into the source document: sourceCharStart and sourceCharEnd in characters, sourcePageStart and sourcePageEnd in pages for sources that were PDFs.
The offsets are the underrated pair. They let you say "this text came from characters 41,220 to 44,905 of that file," which turns a provenance claim into something a reviewer can re-derive rather than take on faith.
Tier 4: currency
The tier where wrong assumptions cost the most. It holds the publisher's own currency statement (currentThrough, currencyNote, currencyYear), the amendment credit and the years parsed out of it (lastAmendedYear, amendmentHistory, amendmentYears, sourceCredit), effective dates where the publisher prints them (effectiveDate, priorEffectiveDates), and two status fields, goodLawStatus for the section and actStatus for the enacting instrument.
Two honest notes about this tier.
First, about a fifth of sections carry no amendment credit at all, because some publishers print none. If you set yearFrom or yearTo, those sections drop out of your results. That is the correct behavior (you asked for an amendment window and they have no amendment date), but it will surprise you if you expected the filter to be a soft preference.
Second, SESSION_LAW rows carry actStatus: "enacted" and goodLawStatus: "unknown" on purpose. Statutes at Large is law as enacted, a historical record, not a statement of what is current. A vendor that stamped those rows "good law" would be telling you something it cannot know.
Tier 5: relationships
Where a section points, and what points at it: crossReferencesUsc, crossReferencesCfr, publicLawCites, relatedCitations, alternateCitations, the supersession chain (supersedes and supersededBy) where the publisher states one, and the move pointers renumberedTo and transferredTo.
Those last two are the ones that save real support tickets. A user pastes a citation they read in a 2014 brief, the section has since been renumbered, and without those fields your app returns "not found" when the correct answer is "it lives here now."
Tier 6: corpus-specific
This is the tier that makes the whole contract necessary, and the Federal Register block is the clearest example. A whole block of fields exists only for Federal Register documents: docket ids and RIN numbers, the comment close date and the effective date, the volume and page coordinates, the significance flag, correction linkage, and the regulations.gov docket page.
Query the US Constitution and every one of those is null. That is not a data gap. A comment close date is a fact about a proposed federal rule and a category error about a constitutional article.
The same logic runs the other way. articleNumber and articleName mean something for a state constitution. ruleSetCode means something for court rules. issuingAgency and lawImplemented mean something for state regulations. caseName and settlementAmount mean something for an HHS OCR resolution agreement and nothing anywhere else. What each family actually lets you build works through them one at a time.
Assert the tiers in your own test suite rather than trusting a changelog:
ALWAYS = {"actId", "citation", "jurisdiction", "corpusType", "title", "externalUrl"}
def check(row: dict) -> None:
missing = ALWAYS - row.keys()
assert not missing, f"tier-1 field vanished: {missing}"
# tier-1 fields may never be null, only absent-by-bug
assert all(row[f] is not None for f in ALWAYS), "tier-1 field went null"
# conditional fields may be null, but must keep their type when present
if row.get("lastAmendedYear") is not None:
assert isinstance(row["lastAmendedYear"], int)
Run it against a fixed set of citations on every deploy. A silent type change on a conditional field is the failure that reaches production, because null is a legal value and nothing errors.
Null is the honest answer
Here is the point most API docs dodge. Vaquill AI's search result object publishes 128 fields, verified against the live OpenAPI document at api.vaquill.ai/external/openapi.json on 2026-08-31. On any single row, most of them are null.
A vendor has two ways to handle that. The dishonest way is to trim the object to a lowest common denominator, so every row looks uniform and you lose the RIN number, the docket id, the transfer target, and the page offsets. The honest way is to publish the full shape and tell you which tier each field sits in.
The trim looks cleaner in a sample response. It is worse in production, because the moment you need the field that was trimmed, you are scraping the publisher's HTML yourself.
There is a real cost to the wide object, and it should be said. A 50-result page ships a lot of null. That is exactly what the fields parameter exists for: ask for the eight fields you render, and the other 120 stop crossing the wire.
Three behaviors that make a contract enforceable
A contract you cannot test is a marketing page. Three specific API behaviors turn the tier table above into something your CI can assert.
Unknown fields names return 422, not silence. Ask for fields: ["citaton"] and the request fails, and the error lists every valid name. Compare that to an API that silently returns a row without your typo'd field. Your code sees a missing value and cannot tell a typo from a genuinely null field. Loud beats convenient here every time. Every error shape is listed in the error reference.
Unknown filter values are rejected, not treated as no match. Pass source: "fars" when you meant far and you get a 422 whose message lists every valid code. An API that returns zero results instead tells you the corpus is empty, which is a much more expensive thing to be wrong about. It also means the error message doubles as documentation.
Paging is cut from one ranking, per the pagination rules. Results never repeat and never go missing between pages, and page 4 costs the same as page 1. If a vendor re-ranks per request, deep paging silently drops rows, and you will only find out when a customer says a section is missing that you can see in the UI.
Where the contract ends
A contract is only credible if it also says what is out of scope, so here is the honest boundary.
Versioning here is the amendment record, not a date parameter. Each citation maps to a single stored text, the one in force now. You can ask what the last amendment year was, filter a search to a year window, read the amendment history, and get per-section diffs on a watched board when something changes. You cannot ask for the text of a section as it read on a given date in 1998. If a vendor claims that, ask them to run it live on a state statute in front of you.
relevanceScore is not a confidence score. It is a relative ranking signal inside one response, so do not compare it across queries or set a fixed quality threshold on it. There is one exception worth knowing: a query that resolves to an exact citation scores that section 1.0, because it is a certain match rather than a ranked one. It is also null on endpoints that did not rank, like a fetch by id.
Coverage gaps are declared rather than hidden. GET /us/statutes/coverage is free and carries a freshness array that names every paused corpus with its reason, including the ones paused because a publisher's own robots.txt opts automated access out. A gap you can read from the API is worth more than a gap you find in production.
The questions to send your vendor
Copy these into your evaluation doc.
- Which fields are present on every row, regardless of corpus? Name them.
- Which fields are corpus-specific, and which corpus does each one belong to?
- What happens when I request an unknown field name? An unknown filter value?
- Is paging cut from a single ranking, or re-ranked per request?
- Which field carries the publisher's own currency statement, and which one carries your ingest date? (If those are the same field, that is your answer.)
- What does a null in a currency field mean: not applicable, not published, or not extracted?
- Do you have a machine-readable coverage endpoint, and does it declare gaps?
Question 7 is the one that separates a data product from a demo. A vendor whose coverage matrix lives in a PDF sales deck cannot tell your monitoring when a state goes stale.
Vaquill AI's US primary law API publishes all of this in its OpenAPI document, and the ingestion pipeline behind it (bulk-source parsers, scrapers, and the JSONL schema) is open source at open-us-law under a permissive license, so you can read the shape before you ever send a request.
FAQ
What is a schema contract for an API?
A schema contract is a stated guarantee about response shape: which fields appear on every response, which are conditional, and what each one means when populated. It is stronger than a field list because it tells you what you are allowed to depend on. Without one, you are writing code against whatever happened to be in the sample response.
Why are so many fields null in a legal data API response?
Because primary law is not one uniform document type. A Federal Register proposed rule has a docket id, an RIN number, and a comment close date. A state constitution has an article number and none of those. A wide schema with honest nulls is more useful than a narrow schema that drops the fields you eventually need.
Which fields should I treat as required in my data model?
The identifier and the citation. In Vaquill AI's API those are actId and citation, and they are retained even when you prune the response with the fields parameter, because a row without an identifier is unusable and a row without a citation is unattributable. Everything else should be nullable in your model.
Can I construct an actId myself instead of calling search?
No. actId is stable but not guessable, and hand-built ids usually 404. Take it from a search response or a citation-resolve response and store it, then use GET /us/statutes/section/{actId} for direct retrieval afterwards.
How do I filter US statutes by amendment year?
Use yearFrom and yearTo on the search endpoint. They filter on the last amendment year the publisher credits, so they track the law rather than the vendor's rebuild date. Note that roughly a fifth of sections carry no amendment credit at all, and those are excluded once either bound is set.
Does any legal API support point-in-time retrieval of statute text?
Vaquill AI's does not, and you should ask any vendor claiming it to demonstrate it live on a state statute. What Vaquill AI offers instead is amendment history per section, a lastAmendedYear value, a year-range filter, change events captured on each refresh, and per-section diffs on watched boards.
What is the difference between currentThrough and lastAmendedYear?
currentThrough is the publisher's own statement of how current the whole code edition is. lastAmendedYear is the last amendment the publisher credits on that specific section. A section can sit in an edition current through 2026 and still have a last amendment year of 1974.
How do I reduce response size when most fields are null?
Pass a fields array on the search request listing only what you render. Unknown names are rejected with 422 rather than silently dropped, and actId and citation are always returned on top of your selection. On a 50-result page this is the difference between shipping 128 keys per row and shipping ten.
How can I check a vendor's coverage claims before signing?
Ask for a free, machine-readable coverage endpoint. Vaquill AI exposes GET /us/statutes/coverage with per-jurisdiction counts, a freshness array that declares every paused corpus with its reason, a currency block quoting each publisher's own currency statement, and a measuredAt timestamp. A coverage claim you can poll beats one you have to trust.
New legal AI guides, weekly.
Further Reading
Hybrid Search Over 12 Million Legal Passages: Why Semantic Alone Fails on Law
Read postParsing Legal Citations in Code: Bluebook Forms, State Variants, and the Ones You Must Refuse
Read postLegal Citation Resolution: Turning a Cite Into the Right Section, Every Time
Read post128 Fields on One Statute: What Rich Legal Metadata Actually Buys You
Read postFrom a State Website to an API Response: Every Layer a Legal Corpus Passes Through
Read postAmendment History and Point-in-Time Law: What a Legal API Can and Cannot Tell You
Read post
Co-Founder & CTO
Priyansh leads engineering and AI at Vaquill, from the matter workbench to drafting, document comparison, document matrix, and citation-verified research.