---
title: "Get a state session law's text, current or a past version"
method: GET
path: "/api/v1/us/session-laws/{session_law_id}/body"
tags: ["US Session Laws"]
---

# Get a state session law's text, current or a past version

`GET /api/v1/us/session-laws/{session_law_id}/body`

## Request

- Base URL: `https://api.vaquill.ai`
- URL: `https://api.vaquill.ai/api/v1/us/session-laws/{session_law_id}/body`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-Key`
- TypeScript: <https://skmtc.dev/vaquill/apis/vaquill-developer-api/docs/api/v1/us/session-laws/:session_law_id/body/get.ts> (a function with typed inputs; its response is checked against the schema)

The act's text exactly as printed, one page of about 30,000 characters at a
time, cut at a paragraph break where one is near. Follow `nextCursor` while
`hasMore` is true; `pageStart` / `pageEnd` / `totalChars` are character offsets
into the whole text, and concatenating the pages in order reproduces it.

**Cost**: 6 credits per page served; each page is charged on its own, so you can stop
whenever you have enough. `pagePrice` is what this page cost, `totalPages` the estimated pages in
all, and `estimatedRemainingCredits` the pages still to come times the price.

**Cost of reading a law.** Reading a whole law costs `estimatedBodyPages` times the price
of one body page (6 credits). `estimatedBodyPages` is on the record next to `charCount`
and is the most pages the text can take: a page holds up to 30,000 characters and is cut
back to a paragraph break, so a long act usually takes fewer. It is null when the text cannot be
read (it can be when served, or when withheld for a reason we label and `/body` reads with
`includeUnverified`). Most acts take a page or two. Each body response carries `pagePrice` and
`estimatedRemainingCredits`, so you can stop whenever you have enough.


Worked example: the longest act we hold, Ohio's 2025 budget act (HB 96, 9,571,132
characters), takes at most 399 pages, 2,394 credits to read in full.

**Where a page starts.** Follow `nextCursor` from the page before, or send `startChar`, a
character offset into the text (0-based, the scale of `pageStart` and `totalChars`), to jump to a
place you know. Send one or the other, never both (a 422, uncharged). A `startChar` page costs and
sizes the same as any other. `startChar` has no version binding of its own, so add
`expectTextSha256` (the `textSha256` you read the text under): if the text was re-collected since,
the answer is a 409 and nothing is charged. There is no page-size parameter: a page is always about
30,000 characters, so its price is fixed.

**Attribution.** Every outcome carries `citation`, `jurisdiction`, `session`, `approvedDate` and
`sourceUrl` (the official copy on a government host, present when the text is withheld or not held
too), so a page is attributable on its own.

**Id only.** This route takes a `sessionLawId`, not a citation: resolve a citation once with
`GET /us/session-laws/{sessionLawId}` and use the id it returns.

**Not served, not charged.** When we do not serve the text (`textStatus` is not `held`), the answer
is a 200 with `available: false`, a `reason` (`not_held`, `withheld`, `pending` or `empty_text`),
`creditsConsumed: 0`, the `withheldReason` for a withheld law, and the official `sourceUrl`. A
federal id (`SAL_...`) is a 404 (`federal_session_law`); read federal session laws with
`GET /us/statutes/section/{actId}`. Errors (unknown id 404, stale cursor or mismatched
`expectTextSha256` 409, malformed cursor or `startChar` 422) are not charged, and a failure on our
side (500, 503) is refunded. The parameters and the 409 response give the exact conditions.

**Past versions: `version`.** We keep every version of a law's text that we served or adopted. List
them with `GET /us/session-laws/{sessionLawId}/versions`, then send one of those numbers as
`version`; omit it for the current version. The response then names `version` and
`isCurrentVersion`, and the page, `nextCursor` and `textSha256` belong to that version. The price is
unchanged: 6
credits per page, whichever version you read. A `version` the law does not have is a 404, refunded.
Without `version` and `includeUnverified` the response is the plain current-text page.

**Text we hold back: `includeUnverified`.** A law whose text has a defect we have not cleared is
`withheld`, and by default `/body` answers `available: false`, free. Send `includeUnverified=true`
to read its text when the reason it is held back is one we can label. The page then carries
`textReliability` (`unverified`: a check has not been completed, or `known_issue`: we have evidence
of the defect), a stable `reasonCode` and a one-sentence `notice` to show beside the text: the text
may show deleted (struck) or vetoed law as if it were enacted, so check the printed act at
`sourceUrl` before relying on it. The codes that serve the text, flagged, are `markup_unverified`,
`strike_unchecked`, `struck_text_shown_as_current`, `struck_text_merged`, `veto_unchecked` and
`licence_unconfirmed`. Some text is not served even when you opt in, and answers
`available: false`, free, with the `reasonCode` and no text: a law whose licence bars use
(`licence_barred`), text that failed our own validation (`validation_failed`), a session we cannot
yet tell apart from another (`session_unresolved`), a source we have not yet audited
(`collector_unaudited`) and a reason we have not classified (`unclassified`). A served law with
`includeUnverified=true` reads as usual and says `textReliability: verified`. Such a page costs
6 credits like any other and can be combined with `version`. The fields `version`,
`isCurrentVersion`, `textReliability`, `reasonCode` and `notice` are omitted unless you asked for them.

**Amendatory markup.** An amending act prints the language it deletes and inserts. Where
`amendatoryMarkup` is `preserved`, that markup is in `text` in the convention `markupConvention`
names: `wdiff` writes deleted language `[-like this-]` and inserted language `{+like this+}`;
`publisher_literal` keeps the publisher's own markers as printed (Minnesota's `new text begin` and
`deleted text begin`); `mixed` has both. Where it is `lost`, the publisher printed markup our text
does not keep, so deleted words can read as law: check the source before quoting. Only the as-printed
text is served; there is no clean "as enacted" view.

**Example**

```bash
curl "https://api.vaquill.ai/api/v1/us/session-laws/SSL_MN_2025S1_G_Y2025_C1/body" \
  -H "Authorization: Bearer $VAQUILL_API_KEY"

# jump to a character offset, pinned to the version of the text you read
curl "https://api.vaquill.ai/api/v1/us/session-laws/SSL_MN_2025S1_G_Y2025_C1/body?startChar=29987&expectTextSha256=02693d5b55d4709b" \
  -H "Authorization: Bearer $VAQUILL_API_KEY"

# an older version (list the versions first at .../versions), or a withheld law's labelled text
curl "https://api.vaquill.ai/api/v1/us/session-laws/SSL_MN_2025S1_G_Y2025_C1/body?version=1" \
  -H "Authorization: Bearer $VAQUILL_API_KEY"
curl "https://api.vaquill.ai/api/v1/us/session-laws/SSL_LA_2025R_G_C2/body?includeUnverified=true" \
  -H "Authorization: Bearer $VAQUILL_API_KEY"
```

```javascript
const MY_BUDGET = 100; // the most you will spend to finish reading, in credits
let cursor;
let text = "";
do {
  const url = new URL("https://api.vaquill.ai/api/v1/us/session-laws/SSL_MN_2025S1_G_Y2025_C1/body");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.VAQUILL_API_KEY}` },
  });
  const page = await res.json();
  if (!page.available) break; // withheld or not held: page.sourceUrl is the official copy
  text += page.text;
  if (page.estimatedRemainingCredits > MY_BUDGET) break; // stop whenever you have enough
  cursor = page.nextCursor;
} while (cursor);
```

**Related endpoints**

- `GET /us/statutes/resolve` answers a citation that names a state session law rather than a
  code section with a `sessionLaw` block (`matched`, or `ambiguous` with every candidate),
  pointing here. It never changes `resolved` or `section`.
- `GET /us/statutes/coverage` lists, per jurisdiction under `sessionLaws`, the sessions held and
  whether each is `complete` or `partial`. A law missing from a `partial` session may still exist.
- Federal session laws (the Statutes at Large, `SAL_` ids) are read with
  `GET /us/statutes/section/{actId}`, not here.

**Nulls.** Every response field is present: one with no value is `null` (`[]` for a list). The
examples here leave the nulls out. `publisher`, `provenance` and `session.coverageStatus` on a law,
and `coverage` on a list page with rows, are absent when they do not apply.

**Rate limits.** Calls count against your key's per-minute budget like every Data API route; a 429
carries `Retry-After`.

## Path parameters

- `session_law_id` string, required: A state session law's id, for example `SSL_MN_2025S1_G_Y2025_C1`, from a `/us/session-laws/list` result. A citation also works here on `GET /us/session-laws/{sessionLawId}`, and `resolvedFrom` says what your input matched. Name the state in the citation: `Minn. Laws 2025, ch. 1`, `2025 Minn. Laws ch. 1`, `Ch. 1, Laws of Minnesota 2025`, `Ch. 2025-12, Laws of Fla.`, `2025 Wash. Sess. Laws ch. 12`, `La. Acts 2025, No. 12`, `Pa. Act 12 of 2025`, `Minn. S.F. 1552 (2025)`, `MN: Laws 2025, ch. 1`. A form only one state uses needs no state: `P.A. 104-0001` (Illinois), `Pub. Act No. 26-151` (Connecticut), `S.L. 2025-12` (North Carolina), `Stats. 2025, ch. 12` (California), `D.C. Law 25-713`. The publisher's printed form (`CHAPTER 2025-12`, `S.F.No. 1552`) and the `citation` we print on every law also work. A form several states share (`Laws 2025, ch. 1`, `HB 123`, `Act 12 of 2025`) is a 404 with `reason: needs_jurisdiction`, the states it exists in and one candidate per state: we never guess the state. A citation naming several laws of one state (Minnesota 2025 has two Chapter 1s) is a 404 listing all of them, `candidateCount` and `truncated` saying whether the list is complete, and `defaultSessionLawId` the regular session's law where there is exactly one conventional reading. An id or citation that matches nothing in a session we have only partly collected is a 404 with `reason: session_not_fully_collected` and that session's coverage: the law may exist and not be held yet, so it is never charged, the resolve price included. A citation costs the route's price PLUS the `/us/statutes/resolve` price (2 credits), the second kept whether or not it resolves (an unresolved or ambiguous citation is a charged verdict) and refunded only on a partly collected session, on a citation we could not read as a state session-law citation at all (its detail lists the accepted forms) or on a failure. URL-encode the citation; one containing `/` cannot travel in a path segment. Federal session laws (`SAL_...`, `Pub. L. 118-5`, the Statutes at Large) are served by `/us/statutes/section`, not here, and code citations (`26 U.S.C. § 1`) by `/us/statutes/resolve`.

## Query parameters

- `cursor` string, nullable: Where to resume: the previous page's `nextCursor`, unchanged. Omit for the first page. It is opaque, signed and tied to the exact text it was cut from: a value we did not issue (`reason: invalid_cursor`), from an older cursor format (`cursor_expired`) or past the end of the text (`cursor_past_end`) is a 422, and one from before the text was re-collected is a 409 (start again without it). None is charged. Send either `cursor` or `startChar`, never both (a 422, never charged).
- `startChar` integer, nullable: Start the page at this character offset into the text (0-based, the same scale as `pageStart` and `totalChars`): an alternative to `cursor` for jumping to a place you already know, such as where a section begins. The page is the same size and price as any other and its `nextCursor` is a normal signed cursor. Send either `startChar` or `cursor`, never both (a 422, never charged). 0 to 100,000,000; a value at or beyond the end of the text is a 422 and is refunded. There is no page-size parameter: the price per page is fixed. A short law is one page whatever you pass, so jumping into it saves no credit.
- `expectTextSha256` string, nullable: The `textSha256` you read the text under, in lowercase hex (the whole 64 characters, or any prefix of 16 or more). When this law's text no longer has that hash (it was re-collected), the answer is a 409 and nothing is charged, exactly as for a `cursor` cut from an earlier version: use it with `startChar`, which otherwise has no version binding of its own. Anything that is not 16 to 64 lowercase hex characters is a 422.
- `version` integer, nullable: Read this version of the act's text instead of the current one: a `version` from `GET /us/session-laws/{sessionLawId}/versions`. Omit for the current version. The page, its `nextCursor` and `textSha256` belong to that version's text, so a cursor from one version is a 409 on another. A number that is not one of the law's listed versions is a 404 and is refunded; a value below 1 is a 422 and is never charged. Priced as any body page. On a law we hold back, an older version is readable only with `includeUnverified=true`, exactly as its current text is.
- `includeUnverified` boolean: Opt in to the TEXT of a law we hold but do not serve (`textStatus: withheld`). Default `false`: such a law answers `available: false`, as before, and nothing else changes. With `true` its text is returned when the reason it is held back is one we can label, and every page then carries `textReliability` (`unverified` or `known_issue`), `reasonCode` and a `notice` to show beside the text: the text may show deleted or vetoed law as if it were enacted. A law whose licence bars use, or whose text failed our own validation, is never served, opted in or not (`available: false` with a `reasonCode`). Priced as any body page. A served law answers as usual, with `textReliability: verified`.

## Headers

- `Idempotency-Key` string, nullable: Optional. 16 to 255 visible ASCII characters (a UUIDv4 works). Repeating the same request with the same key replays the original 2xx response for 24 hours without charging again. The same key on a different request is rejected. With the account's `cacheQueryDerivedData` retention setting off, no body is stored and a retry answers 409 `idempotency_replay_unavailable`, still without a second charge.

## Response `200`

One page of the act's text, or `available: false` (not charged) when we do not serve it.

- SessionLawBodyResponse: Response for `GET /us/session-laws/{sessionLawId}/body`.
  - `processingTimeMs` number: Server-side time for this request in milliseconds, excluding network transit. Not billed on.
  - `creditsConsumed` integer: Credits actually charged for this call, never the list price. It includes the `/us/statutes/resolve` fee when a citation was sent where an id goes. 0 when the call was not charged or was refunded (a body that answers `available: false`).
  - `sessionLawId` string, required: The law this page belongs to.
  - `citation` string, required: The act's citation as its publisher prints it, the same as `citation` on `GET /us/session-laws/{sessionLawId}`. Carried here so a page is attributable on its own, with no second call.
  - `jurisdiction` string, required: Lowercase two-letter code of the state, `dc` or `pr`.
  - `session` SessionLawSession, required: The legislative session a law was enacted in.
    - `code` string, required: Session code, `{year}{type}{ordinal}`. `type` is `R` regular, `S` special, `X` extraordinary, `F` fiscal, `V` veto, `U` unknown. A regular session has no ordinal (`2025R`); a numbered special session carries it (`2025S1`, the first special session of 2025); a lettered one carries its letter (`2025SC`, Florida's Special Session C). `year` is the year the publisher numbers the session by, which for a period spanning two years is the first (District of Columbia Council Period 25 is `2023R`). Pass it as `session` to `GET /us/session-laws/list`.
    - `label` string, nullable: The session's canonical label, as the publisher names it.
    - `type` string, nullable: `regular`, `special`, `extraordinary`, `fiscal`, `veto` or `unknown`.
    - `year` integer, nullable: The year the session convened, the same year `code` starts with.
  - `approvedDate` string, date, nullable: Date the governor approved the act, ISO 8601, as on `GET /us/session-laws/{sessionLawId}`: never an effective date. Null when none is held. See `approvedDateDerived`.
  - `approvedDateDerived` boolean: True only when `approvedDate` was filled from the publisher's dated signature label rather than held as the approval date. See the same field on `GET /us/session-laws/{sessionLawId}`.
  - `sourceUrl` string, nullable: The official page or file this text was collected from, on a government publisher's host. Present on EVERY outcome, a withheld or not-held law included, so a reader is never at a dead end: where we do not serve the text, this is where to read it. Null only when we hold no government text source for the law.
  - `officialUrl` string, nullable: The publisher's page for this act (never a Vaquill URL): the legislature's page for the bill where it keeps one, else `sourceUrl`. See the same field on `GET /us/session-laws/{sessionLawId}`.
  - `available` boolean, required: False when we do not serve this law's text; `reason` says why. A page with `available: false` is not charged.
  - `reason` string, nullable: Why `available` is false: `not_held`, `withheld`, `pending` or `empty_text`. Null when the text is served.
  - `textStatus` 'held' | 'withheld' | 'not_held' | 'pending', required: Whether we serve this law's text. `held`: the text is served by `/us/session-laws/{sessionLawId}/body`. `withheld`: we hold the text but a measured defect keeps it from being served, and `withheldReason` says what. `not_held`: we know the act exists and do not hold its text. `pending`: collected and not yet verified.
  - `withheldReason` string, nullable: Present only when `textStatus` is `withheld`: what is wrong with the text we hold. A plain sentence to show, not an enum to branch on.
  - `text` string, nullable: This page of the act's text, exactly as printed, in reading order. Concatenating every page's `text` in order reproduces the whole text. Where `amendatoryMarkup` is `preserved`, struck and inserted language is in the text in the convention `markupConvention` names. Null when `available` is false.
  - `pageStart` integer, nullable: Character offset in the full text where this page starts (0-based). Pages are contiguous: the next page's `pageStart` equals this page's `pageEnd`.
  - `pageEnd` integer, nullable: Character offset just past this page. A page is about 30,000 characters, cut back to a paragraph break where one is near, so the length varies.
  - `totalChars` integer, nullable: Length of the whole text in characters. This page is the last when `pageEnd` equals it.
  - `totalPages` integer, nullable: The most pages the whole text can take: a page holds up to the page size but is cut back to a paragraph break, so every page but the last is counted at its shortest size. A long act usually takes fewer pages and never more. Null when no text is served.
  - `pagePrice` integer: Credits charged for the page in this response, the same as `creditsConsumed` on this route: the price of one page when a page was served, 0 when none was (`available` is false). Each further page costs the same.
  - `estimatedRemainingCredits` integer, nullable: The most credits to read the rest of the text from `pageEnd`: the most pages still to come (counted at the shortest size a page can be cut back to) times the price of a page. 0 on the last page. Null when no text is served. An upper bound that is never below what reading the rest costs. Reading a whole law costs `estimatedBodyPages` times the price of one body page (6 credits per page on `GET /us/session-laws/{sessionLawId}/body`); most acts are a page or two.
  - `textSha256` string, nullable: SHA-256 (hex) of the full text of this law. It changes only when the text does, so it is the cheap way to tell whether a copy you hold is current. Body pages carry the same value.
  - `amendatoryMarkup` string, nullable: Whether the language an amending act strikes and inserts survives in the text. `preserved`: it is in `text`, in the convention `markupConvention` names. `lost`: the publisher printed it and our text does not keep it, so deleted words can read as law: check the source before quoting. `absent`: the act carries no such markup that we found. `not_applicable`: there is no text to carry it.
  - `markupConvention` string, nullable: How preserved markup is written, set only when `amendatoryMarkup` is `preserved`. `wdiff`: deleted language is `[-like this-]` and inserted language `{+like this+}`. `publisher_literal`: the publisher's own words are left as printed (Minnesota prints `new text begin` / `new text end` and `deleted text begin` / `deleted text end` markers). `mixed`: both in one text.
  - `nextCursor` string, nullable: Opaque cursor for the next page: send it as `cursor` on the same path. Null on the last page. It is tied to this exact text, so a cursor from before the text was re-collected is a 409.
  - `hasMore` boolean: True when more pages follow; fetch them with `nextCursor`. Each page is charged.
  - `version` integer, nullable: Which version of the act this page belongs to, the `version` of `GET /us/session-laws/{sessionLawId}/versions`. Present only when you sent `version` or `includeUnverified`; absent otherwise (the current version).
  - `isCurrentVersion` boolean, nullable: True when `version` is the version `GET /us/session-laws/{sessionLawId}` serves now. Present with `version`.
  - `textReliability` 'verified' | 'unverified' | 'known_issue', nullable: How far to trust this text. Present when you sent `includeUnverified=true`, and on every read whose text is not `verified`. `verified`: the text passed our strike-through check. `unverified`: a check has not been completed, so the text MAY show deleted or vetoed law as if enacted. `known_issue`: we have evidence it does. A served law can be any of the three and its text is served either way. Branch on `reasonCode`, show `notice`.
  - `reasonCode` string, nullable: Present when the text is not `verified`, or when `includeUnverified=true` refuses it: a stable token, one of `markup_unverified`, `strike_unchecked`, `struck_text_shown_as_current`, `struck_text_merged`, `veto_unchecked`, `licence_unconfirmed`, `markup_contradicted`, `struck_text_evidenced`, `markup_lost`, `strike_unconfirmed` (the text is served with a flag) or `validation_failed`, `session_unresolved`, `collector_unaudited`, `licence_barred`, `unclassified` (the text is not served). The sentence in `withheldReason` is for people; branch on this.
  - `notice` string, nullable: One plain sentence saying what is uncertain about this text and what to do about it. Present exactly when `textReliability` is `unverified` or `known_issue`. Show it beside the text.

## Other responses

- `401`: Missing, invalid or revoked API key. Not charged.
- `402`: Insufficient credits for this call. Nothing was done and nothing was charged. Read your balance, free, at `GET /credits/balance`.
- `403`: The API key lacks the `research:read` scope. Not charged.
- `404`: Unknown id, a federal act, or an endpoint name where the id goes. `reason` says which. Never charged.
- `409`: The `cursor` was cut from an earlier version of this law's text, or the `expectTextSha256` you sent is not its hash: the text has since been re-collected. Not charged. Start again without `cursor`. Idempotency conflict. `idempotency_in_progress`: the original request with this key is still running (retry after `Retry-After`). `idempotency_replay_unavailable`: it completed but its response was too large to replay; nothing is charged again, so check the credit balance instead of retrying.
- `422`: A parameter failed validation: a `cursor` we did not issue, `cursor` sent with `startChar`, or a malformed `startChar` or `expectTextSha256`. Never charged. `errors[].loc` names the parameter exactly as you send it. `idempotency_key_reused`: the `Idempotency-Key` was already used with a different request.
- `429`: Rate limit exceeded for your key. Not charged. Wait `Retry-After` seconds.
- `500`: An unexpected failure on our side. The charge is refunded.
- `503`: The session-laws database is temporarily unavailable. The charge is refunded; retry.

## Changes

- **2026-11-07** `340c0d24d0b6`: 72 info
  - added the optional property `candidateCount` to the response with the `401` status
  - added the optional property `candidateCount` to the response with the `402` status
  - added the optional property `candidateCount` to the response with the `403` status
  - added the optional property `candidateCount` to the response with the `409` status
  - …68 more

[Operation history](https://skmtc.dev/vaquill/apis/vaquill-developer-api/changes/api/v1/us/session-laws/:session_law_id/body/get.md)

---

[API](https://skmtc.dev/vaquill/apis/vaquill-developer-api.md) - [All operations](https://skmtc.dev/vaquill/apis/vaquill-developer-api/llms.txt) - [OpenAPI document](https://skmtc.dev/vaquill/apis/vaquill-developer-api/revisions/340c0d24d0b6?raw)
