What a Legal Data API Should Guarantee: The Schema Contract

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 isn't better error handling. It is a stated contract you can hold the vendor to.

Reviewed and corrected on 12 October 2026. This is general information, not legal advice.

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 148 fields, and exactly one of them (actId) is schema-required. That asymmetry is the whole design.
  • Three API behaviors make a contract enforceable: unknown fields names 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.
  • Point-in-time retrieval deserves a live demo. An earlier text can be rebuilt from recorded changes, but how far back that reaches depends on when capture began. Ask any vendor which dates return a real answer and which come back unavailable.
Spot the problem
Question 1 of 4

You build against a legal API by mapping the fields in one sample response for a federal statute. Six weeks later you add state codes and your parser starts throwing. What was the underlying mistake?

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 isn't a longer field list. It is a statement about which fields are load-bearing and which are optional by design.

A short history: law in XML, twice

Machine-readable law is older than most APIs, and it came in two flavors that don't match.

In the US the legislative branch picked XML early. The House's own technology timeline records that "In July, the Law Revision Counsel made the U.S. Code available in XML," and calls it the first release of the "generation two" schema, United States Legislative Markup, or USLM (House XML Technology Timeline). A Hacker News thread titled "The entire US Code is now online in XML" is dated July 31, 2013. One commenter's reaction was dry: "It took the US Govt 17 years to release 200,000 pages of the US code in XML." (liscovich, Hacker News, July 2013) The 17 years and the page count are the commenter's own arithmetic, so take them as a mood rather than a statistic.

The schema kept moving. In August 2018 GPO, the Clerk of the House, the Secretary of the Senate and the Office of the Federal Register were running parallel projects to convert a subset of enrolled bills, public laws, the Statutes at Large, the Federal Register and the Code of Federal Regulations into USLM XML, and a draft of the USLM 2.0.0 schema went onto GPO's GitHub repository for comment (infoDOCKET, relaying FDLP.gov, August 2018).

Outside the US, a different standard got there by another road. Akoma Ntoso was approved as an OASIS Standard by the members on 28 August 2018. The name "means 'linked hearts' in the Akan language of West Africa where the original work was done," and OASIS describes it as a framework "for creating machine-readable parliamentary, legislative and judiciary documents such as legislation, debate records, minutes, judgments, etc." (OASIS announcement, September 2018) The specification's contributor list reads like a map of who needed this: staff from the parliaments of Uganda and Uruguay, the Italian Senate, the National Library of Congress of Chile, the US Library of Congress, and Cornell's Legal Information Institute (Akoma Ntoso 1.0, Part 1).

Two serious standards, two ways to say "this is a section of a statute." That's the useful lesson for an API consumer. A markup standard says how a document can be tagged. It says nothing about which tags a given publisher bothered to fill in, which is why the tiers below matter more than the format the data arrived in.

There's a name for why a sample response quietly becomes the contract. Hyrum Wright, who now works at Adobe and earlier wrote infrastructure code at Google, put it this way: "With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody." (Hyrum's Law) He credits Titus Winters with naming and popularizing it. The corollary for a data vendor is uncomfortable. If you don't write the contract down, your customers' code writes one for you, and it's whatever the first sample happened to show.

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.

Loading diagram...

Tier 1: always present

Two fields, and only two.

FieldTypeGuarantee
actIdstringThe only schema-required field. Stable identifier for one citable section.
citationstringAlways 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 aren't 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 56 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 doesn't 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, with sourceUrl as the link to cite), 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. Where populated, they let you say "this text sits at these characters of the source file," which lets a reviewer reconcile our extraction against the original. They are null on many rows (all three rows we pulled on 12 October 2026), and the API reference documents the page fields as 100 percent populated on the Federal Rules (on 12 October 2026 Fed. R. Evid. 801 and Sup. Ct. R. 13 and 14 carried them, while Fed. R. Civ. P. 12 and Fed. R. Crim. P. 12 returned null), so treat them as corpus-specific.

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: actStatus, the publisher's own status word for the section, and goodLawStatus, our conservative verdict derived from it (good_law, not_good_law, not_operative or unknown).

Two honest notes about this tier.

First, some sections carry no date the year filter can read, because some publishers print no amendment credit. The filter reads the last amendment year, else the effective year, else (for corpora dated by publication) the publication year. If you set yearFrom or yearTo, sections with none of those drop out of your results. That is the correct behavior, 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 (we confirmed this on SAL_PL116-136_DVA_TII_S1109 on 12 October 2026). 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 can't 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 isn't 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"}

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
    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)

We ran this against the real 42 U.S.C. § 1983 result from 12 October 2026 and it passes. A row missing citation fails it, as intended. Everything outside the two tier-1 fields is deliberately not asserted as present.

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 148 fields, counted against the live OpenAPI document at api.vaquill.ai/external/openapi.json on 2026-10-12. 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 rest stop crossing the wire.

Three behaviors that make a contract enforceable

A contract you can't 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 can't 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 a later page costs no more than the first. Depth is capped: limit tops out at 50 and offset at 70, so the deepest reachable result is 120 (offset plus limit), and hasMore tells you when there is nothing further. 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.

Earlier text comes from one of two engines. The section body endpoint takes an asOf date. Where we hold the publisher's printed editions (the CFR annual editions, the US Code, the Copyright Compendium and OFAC FAQs; for the US Code most historical dates fall outside the editions we hold), it returns one verbatim, and a date between held editions comes back unavailable. For every other corpus the earlier text is rebuilt from the changes recorded after your date. The asOf block says which engine answered and whether the answer is bounded by when capture began rather than by the law, so read that before you cite the text. Alongside it sit the last amendment year, a year-window filter, the amendment history, and per-section diffs on a watched board. If a vendor claims to return text as it read on a given date in 1998, 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 don't 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 didn't rank, like a fetch by id.

Coverage gaps are readable rather than hidden. GET /us/statutes/coverage is free (it needs an API key) and lists, per jurisdiction, only the corpora we hold, with counts and a statusBreakdown, plus a measuredAt timestamp. GET /boards reports how recently each source was read (lastRetrievedAt) next to its declared cadence. 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.

  1. Which fields are present on every row, regardless of corpus? Name them.
  2. Which fields are corpus-specific, and which corpus does each one belong to?
  3. What happens when I request an unknown field name? An unknown filter value?
  4. Is paging cut from a single ranking, or re-ranked per request?
  5. 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.)
  6. What does a null in a currency field mean: not applicable, not published, or not extracted?
  7. 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 can't tell your monitoring when a state goes stale.

Vaquill AI's US primary law API publishes the response shape in its OpenAPI document, and the collection code and schema are open source at open-us-law under Apache-2.0, 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.

Because primary law isn't 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.

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. Sections with no date the filter can read (some publishers print no amendment credit) are excluded once either bound is set.

Vaquill AI's section body endpoint takes an asOf date. It returns a held published edition where one exists (CFR annual editions, the US Code, the Copyright Compendium and OFAC FAQs) and otherwise rebuilds the earlier text from the changes recorded after that date. The response says which engine answered and whether the answer is bounded by when capture began, so check that before citing it. Whichever vendor you ask, have them run it live on a state statute.

What is the difference between currentThrough and lastAmendedYear?

currentThrough is the date the publisher states the text is current through. 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 148 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 by corpus, a statusBreakdown of each corpus by status, and a measuredAt timestamp, and GET /boards with each source's cadence and lastRetrievedAt. A coverage claim you can poll beats one you have to trust.

Connect our US primary law database.
Every US statute, regulation, constitution, and executive order via REST, MCP or SQL. 5.4M+ sections, section-level citations, and links to the official source. Plus a free open dataset.
Updated October 12, 202623 min read

New legal AI guides, weekly.

Priyansh Khodiyar

Priyansh Khodiyar

Co-Founder & CTO

Priyansh leads engineering and AI at Vaquill AI: the pipelines that pull statutes, regulations and court rules from every US jurisdiction's official publisher, and the REST API, MCP server and open dataset that serve them.