# US Census of Governments Finance API — Data Dictionary

Public, read-only JSON API over the Civilytics COG finance corpus.
**Scope:** government types 0–3 (state, county, municipality, township). The
published fiscal-year range is served from the corpus manifest — read the
`scope` field at `/api/v1/`. All amounts are in **full US dollars** (the raw
Census `$1,000s` are already multiplied up).

Cite: *Civilytics Consulting, US Census of Governments finance corpus.*

## Bulk data — don't page for a dataset

The JSON envelope is built for a screen of results, not a working dataset. If you
are about to write a loop over `page=`, one of these is what you want instead.

**The whole corpus, or any large slice: read the parquet mirror, not this API.**
The corpus is published as hive-partitioned parquet under CC-BY at
<https://huggingface.co/datasets/civilytics/us-cog-finance>. Reading it directly is
faster than any number of API calls, costs the server nothing, and exposes every
column:

```sql
-- DuckDB, no download required
SELECT * FROM read_parquet(
  'hf://datasets/civilytics/us-cog-finance/data/long/**/*.parquet',
  hive_partitioning = true)
WHERE year = 2022;
```

```python
import pandas as pd
df = pd.read_parquet("hf://datasets/civilytics/us-cog-finance/data/long/year=2022")
```

```r
ds <- arrow::open_dataset("hf://datasets/civilytics/us-cog-finance/data/long")
```

The mirror is the **raw long table**. Harmonization, inflation adjustment,
per-capita scaling and provenance are applied on top of it by the
[`uscogdata`](https://gitea.civilytics.org/Civilytics/uscogdata) R package — point that
package at the mirror if you want the API's semantics offline.

**More than a page, less than the corpus: `format=csv`.** `/spending` and `/revenue`
accept `format=csv`, which returns the rows themselves rather than the envelope and
raises the row cap to **50,000 in a single request**:

```
GET /api/v1/spending?state=WI&type=city&years=2022&format=csv&limit=50000
```

| | JSON (default) | `format=csv` |
|---|---|---|
| Body | `{status, data, error, meta, provenance, suggestions}` | CSV rows, header first |
| Max `limit` | 1000 | 50,000 |
| Row count | `meta.total` | `X-Total-Count` header |
| Page / limit | `meta.page`, `meta.limit` | `X-Page`, `X-Limit` headers |
| Provenance | `provenance` block | not included — use JSON if you need it |

The raised cap applies only when `subtype=` and `recipe=` are unset. Those filter
*after* the query returns, so the row limit cannot be pushed into SQL and the cap stays
at 1000 — an over-cap request there is a 400, never a silent clamp. `format=` is
accepted on `/spending` and `/revenue` only; other routes reject it as an unknown
parameter.

## ⚠ Recent breaking change (2026-07-31)

Two figures moved in the 2026-07-31 release. Neither raises an error.

1. **Default revenue is smaller for governments that run utilities.**
   `revenue_concept` now defaults to `general` (strict Census General Revenue),
   so utility, liquor store and insurance trust revenue left the default —
   **15.9% of city revenue** on average. Add `revenue_concept=total` to
   reproduce the previous default exactly.
2. **`expenditure_concept=direct` returns a larger figure.** `direct` now
   includes interest on long-term debt, matching Census's published "Direct
   Expenditure". The *default* is unchanged: the new default `primary` returns
   what the old default returned. Use `expenditure_concept=primary` to keep
   previous numbers under an explicit request.

Both are explained in full below, with worked dollar examples.

## Which `expenditure_concept` do I want?

There are **three** expenditure concepts, nested smallest to largest. The
difference between the first two is **debt service**, and it is the single
most common source of a figure that doesn't tie to a published Census number:

| concept | includes | excludes |
|---|---|---|
| **`primary`** (default) | Running services: salaries, supplies, construction, equipment | Interest on long-term debt; payments to other governments |
| **`direct`** | `primary` **+ interest on long-term debt** (item code `I89`). **This is Census's own published "Direct Expenditure".** | Payments to other governments |
| **`total`** | `direct` **+ payments to other governments** (`Total = Direct + M + L`) | — |

**If your number has to match a Census table headed "Direct Expenditure", ask
for `expenditure_concept=direct` explicitly.** The default is `primary`, which
is smaller by exactly the government's interest on long-term debt. Worked
example — Madison, WI FY2020:

| request | figure |
|---|---:|
| default (`primary`) | $623,347,000 |
| `expenditure_concept=direct` | $651,051,000 |
| difference (`I89`, general interest on debt) | **$27,704,000** (4.4%) |

A government with no long-term debt service in a given year will see
`primary` and `direct` return the same figure; one carrying bonds will not.

**`expenditure_concept=total` is available on exactly *one* endpoint:**
`GET /api/v1/governments/<govid>/spending`. That route takes `govid` as a
**path** parameter (no comma-splitting), so it is single-government by
construction — the only place `total` can never double-count. **Every other
spending endpoint accepts `primary` and `direct` only** — the flat `/api/v1/spending`,
`/api/v1/rollups`, `/api/v1/governments/<govid>/peer-comparison`, and
`/api/v1/governments/<govid>/profile` all reject `total` specifically with an
HTTP 400 naming `/api/v1/governments/<govid>/spending` as the fix, rather
than silently downgrading (`primary` and `direct` both still work on all
four). **Every `/revenue` route rejects the param outright, for ANY value**
— `primary`, `direct`, `total`, or anything else — with its own 400: the
expenditure concepts do not apply to revenue, which has **its own** pair.
See "Which `revenue_concept` do I want?" below. This is a route-level rule, not a runtime one: whether (and
how) `expenditure_concept` is available is predictable from the URL alone,
regardless of how many governments a particular request happens to resolve
to.

1. **"Total spending in my county, 2017 vs. today?"** — one government,
   tracked over time:
   `GET /api/v1/governments/<govid>/spending?expenditure_concept=total`.
2. **"All the counties in my state, ten years ago vs. today, vs. the
   neighboring state?"** — many governments, summed together. **Use
   `primary` (the default) or `direct`**: `GET /api/v1/rollups`. The flat `/api/v1/spending`
   looks like it might work too (it accepts a `govid` list or a `state`/`type`
   filter, so it's a fleet endpoint exactly like `/rollups`) — but it also
   rejects `total` (400), for the same reason.

**Why combining `total` across governments is wrong:** Census `Total` is a
government's own direct spending **plus** the money it hands to other
governments — `Total = Direct + M + L` (`M` = payments to local governments,
`L` = payments to state governments). Say a state gives a county $10M for
roads: the state reports it once, as an intergovernmental `M44` payment; the
county reports the *same* $10M again, as its own direct `E44`/`F44` highway
spending, when it actually pays for the road. Sum `Total` across both
governments and the $10M road shows up as $20M of "road work" — a
double-count, not a bigger number.

**Measured, across every state × year county rollup in the published corpus**
(2,640 rollups, 1967–2023, 48 states): summing `total` across a state's
counties overstates spending with a **median of 3.3%** — but the tail is
long: **p95 is 26.8%, and the worst case is 84.8%** (Montana, 1971). Over 1 in
5 rollups (20%) overstate by more than 10%. **Treat that 3.3% median as
typical, not as a ceiling** — the tail (p95 26.8%, max 84.8%) is real, the
typical case looks small and safe, and nothing in a JSON response marks which
case you're looking at, which is exactly why every multi-government endpoint
refuses `total` outright instead of labelling the risk.

**Legacy years and the intergovernmental-only row.** For some (year,
category) pairs in the legacy wide era (through FY2011), the Census only
publishes an aggregate-flagged family total — the Direct leg has **no rows at
all** for that government/year/category — while the intergovernmental leg
still carries dollars. A `total` query there returns the IG leg **alone**,
not Direct + IG, which can look like a large (10×+) jump or drop between
adjacent years as coverage resumes or lapses. This is flagged, not silent:
`provenance.expenditure_concept_direct_suppressed` (mirrored into
`meta.expenditure_concept_direct_suppressed`, so a JSON-only consumer sees it
without reading `provenance` at all) is `TRUE`, each affected row's `notes`
says so explicitly, `provenance.expenditure_concept_note` explains the
mechanism, and the top-level `suggestions` array names the harmonization
recipe(s) that recover the missing Direct component (and its
intergovernmental-counterpart recipe, where relevant). Always check these
before comparing a `total` figure across years — see "Provenance block" below.

`direct` is the only concept that sums cleanly across governments, which is
why the multi-government endpoints — the flat `/api/v1/spending`,
`/api/v1/rollups`, and `/api/v1/governments/<govid>/peer-comparison` — reject
`total` outright (400) rather than silently pinning to `direct`.
`/api/v1/governments/<govid>/profile` rejects it too, but for an unrelated
reason: it's scoped to a single government like `/spending` is, but blends
spending and revenue into one combined table, and `total` is a spending-only
concept with no revenue-side equivalent.

`subtype=intergovernmental` rows only exist under `expenditure_concept=total`
— requesting that subtype under the default `direct`, or on any endpoint
where `total` isn't offered at all, is a 400 (not a silent empty result); see
"Query parameters" below.

`expenditure_concept` is orthogonal to `basis` — pair `harmonized`/`raw` with
either concept freely on `/governments/<govid>/spending` — but mutually
exclusive with `recipe`: pairing `recipe=` with `expenditure_concept=total` is
a 400 (a recipe is a fixed, pre-merged code bundle; `total`'s
intergovernmental leg has no defined meaning layered on top of one). `recipe=`
pairs fine with the default `direct`.

## Which `revenue_concept` do I want?

Census publishes **two** revenue concepts, related by an identity the
classification manual (§4.3) states by subtraction — *"General revenue
comprises all revenue except that classified as liquor store, utility, or
insurance trust revenue"*:

```
Total Revenue = General + Utility + Liquor Store + Insurance Trust
```

| concept | includes | excludes |
|---|---|---|
| **`general`** (default) | `own_source` + `federal` + `state` + `local_aid` — taxes, charges, and intergovernmental aid | Utility, liquor store, and insurance trust revenue |
| **`total`** | Everything: `general` **plus** utility (`A91`–`A94`), liquor store (`A90`), and insurance trust revenue | — |

**This distinction matters most for cities**, which commonly operate water,
electric, gas, or transit utilities. Utility + liquor store revenue as a share
of what a government collects, measured across the corpus:

| government type | utility + liquor store share |
|---|---:|
| **City** | **15.9%** |
| Township | 4.8% |
| County | 1.7% |
| State | 1.2% |

Worked example — Madison, WI FY2020: `general` **$564,322,000** vs `total`
**$616,551,000**, a difference of $52,229,000 in utility revenue.

So a city's *general* revenue is materially smaller than its *total* revenue.
If you are comparing against a published figure, check which concept it used:
a budget document's "total revenue" is usually Census `total`, while
state-by-state general-revenue comparisons are `general`.

### Why revenue `total` has no route restriction (and expenditure `total` does)

`expenditure_concept=total` is confined to a single-government route because
it adds money the government hands to **other** governments, which those
governments report *again* as their own spending — so summing it across
governments double-counts real dollars.

`revenue_concept=total` has no such hazard. It adds the government's **own**
utility, liquor store and insurance trust receipts, which no other government
reports. Summing revenue `total` across governments double-counts nothing, so
it is available on **every** revenue endpoint. The asymmetry is deliberate.

### Series seam: insurance trust ends at FY2016

The employee-retirement codes inside insurance trust revenue
(`X01`/`X02`/`X05`/`X08`) **stop after FY2016**, when those systems moved out
of the annual finance collection into a separate one. A long
`revenue_concept=total` series therefore **steps down at the FY2016/FY2017
boundary for collection-scope reasons, not because revenue fell.** Do not read
that step as a real decline.

Under the `general` default the seam does not arise: insurance trust is
excluded on both sides of FY2016 either way.

## Response envelope

Every response has the same shape:

```json
{
  "status": "success | error",
  "data":   [ ... ] | null,
  "error":  "message" | null,
  "meta":   { "version": "v1", "total": 22, "limit": 100, "page": 0, ... },
  "provenance": { ... } | null,
  "suggestions": [ ... ]
}
```

- On **success**: `error` is `null`; `data` is an array of row objects; `meta`
  carries paging + any applied filters; `provenance` (present on monetary
  endpoints) records exactly how the numbers were produced; `suggestions` is
  always an array (`[]`, never `null`, when there's nothing to suggest) — see
  "Harmonized basis & recipes" below.
- On **error**: HTTP 400 (bad parameter) or 500; `data` and `provenance` are
  `null`; `error` is a human-readable message; `suggestions` is still `[]`
  (never absent). `meta.version` is always `"v1"`.

### An empty result means exactly one thing

`status: "success"` with `data: []` means **the government reported nothing
matching your filters** — a real and common condition in this corpus. It never
means the govid was wrong, and it never means the year was unavailable. Those
are errors, and each has its own message:

| Condition | Response |
|---|---|
| `govid` that cannot be an id at all | **400** `govid '<x>' is not a canonical_govid. Expected 12 digits (e.g. …).` — a canonical_govid is **digits only, exactly 12 of them**. A URL, a name, or a truncated/FIPS-style id is refused on every route, including the flat `govid=` list (cog-api#55). This previously returned an empty success, making a mis-pasted id indistinguishable from a government that genuinely reported nothing |
| Government not in the registry (well-formed id) | **400** `Unknown govid '<id>'.` on every `/governments/{govid}/...` route — but **not** on the flat routes: there a well-formed unknown id returns `success` and is named in `provenance.scope.govids_missing`, so a ten-id list does not fail because one entry went stale |
| Year outside the published range | **400** `year must be in <min>-<max>.` |
| Year inside the range, no source data | **400** `year <N> is inside the published range <min>-<max> but has no source data. Years with no source data: ...` |
| Year not a number | **400** `year must be a four-digit year in <min>-<max>.` |

The published range is a **set, not a span** — it can have holes, and the
in-range-but-no-source-data case is what those holes produce. `GET /api/v1/`
names them in its `scope` string, from the same source as the error above, so
the two can never disagree.

## Query parameters

### Money / scaling (on every monetary endpoint)

| Param | Values | Default | Meaning |
|---|---|---|---|
| `adjust` | `nominal` \| `real` | `nominal` (`/profile`: `real`) | Inflation adjustment. `real` uses bundled BLS CPI-U (CPIAUCSL). |
| `base_year` | `YYYY` (within the published range) | latest year | Real-dollar base year; used only when `adjust=real`. |
| `per_capita` | `true` \| `false` | `false` (`/profile`: `true`) | Divide by per-year Census F-33 population. |

### Filters

| Param | Values | Notes |
|---|---|---|
| `q` | a place name, e.g. `Madison` | **Government name search — the parameter that turns a place name into a `canonical_govid`, and step one of most workflows here.** `/governments` only. Matches **`gov_name`** as a **case-insensitive substring**, so `q=madison` finds `MADISON CITY`; results are ordered by ACS population, largest first. It is not anchored and not an exact match, so a short `q` can return many rows — narrow with `state` and `type` (`/governments?q=Madison&state=WI&type=city` returns exactly one). **Caveat:** the value is currently interpreted as a regular expression, so metacharacters (`(`, `)`, `[`, `.`, `|`, `*`) are significant and a name containing them may over-match or error; prefer a plain word until that is fixed. Omitting `q` entirely lists every government matching the other filters. |
| `category` | e.g. `Police`, `Property Tax`, or the reserved `All Categories` | **Case-insensitive**; canonicalized against `/categories`. Unknown → 400 with valid options. Mutually exclusive with `recipe`. **Required** on `/governments/{govid}/peer-comparison` (400 if omitted); optional everywhere else, **including `/rollups`** — omitting it there (or passing `All Categories` explicitly) returns an all-categories total, one row per government per subtype, under the reserved category `All Categories` (needs `uscogdata` ≥ 0.2.0; see "`All Categories` is not `expenditure_concept=total`" below). **Every money route rejects a wrong-flow category with a 400 naming its type** (cog-api#41): `/rollups` and `/spending` serve expenditure categories, `/revenue` serves revenue categories, `/governments/{govid}/balances` serves balance categories, and the per-government `/spending` and `/revenue` routes match their flat siblings. So `/spending?category=Property%20Tax` is a 400 pointing at `/revenue`, where it used to be a `200` with `data: []` that read as "no government reported property tax." Two exemptions: the reserved `All Categories` works on every money route regardless of flow, and `/profile` is ungated because it blends both flows into one table by design. |
| `layer` | `state`, `county`, or `city` | **Required** on `/rollups` — it names the geographic level to aggregate to, and there is no default (omitting it is a 400: `layer must be one of state, county, city.`). `/rollups` only. |
| `subtype` | spending: `operations`, `capital`, `assistance`, `interest`, `insurance_benefits`, `intergovernmental` · revenue: `own_source`, `federal`, `state`, `local_aid`, `utility`, `liquor_store`, `insurance_trust` · balances: `general`, `employee_retirement`, `unemployment_trust`, `workers_comp_trust`, `other_insurance_trust` — **see "Which subtypes exist" below: which values can appear depends on the concept you asked for** | **Case-insensitive** row filter. A subtype from the wrong flow → 400. `intergovernmental` rows exist **only** under `expenditure_concept=total` on `/governments/{govid}/spending` (the one endpoint where `total` is available) — requesting `subtype=intergovernmental` without `total`, or on any endpoint that doesn't offer `total` at all, is a 400 (not a silent empty result). Applied at the API layer and echoed in `meta.subtype`. Mutually exclusive with `recipe`. |
| `years` | a year `2022`, a CSV list `2019,2020`, an inclusive range `2000-2010`, or a mix `2000-2010,2022` | Omitting `years` returns the **full published span** on the per-government money routes (`/governments/{govid}/spending`, `/revenue`, `/balances`, `/profile`) — but only the **latest published year** on the fleet routes (flat `/spending`, `/revenue`, `/rollups`, `/peer-comparison`), and the **latest census year** on `/rollups`/`/peer-comparison` specifically when `coverage=census`. A bare year and a range are validated with **different strictness** on purpose: a bare year names one specific year, so a year with no source data is still a 400 even though it sits inside the published range; a range's two **endpoints** only need to fall inside the published span — `years=1967-2024` succeeds despite FY1968/FY1969 having no source data, because the range resolves to whatever the corpus actually holds inside it (holes dropped, never invented). A range is a 400 only when **no** published year falls anywhere inside its span. |
| `state` | 2-letter (`FL`) | Flat queries: resolve a govid set. On `/rollups` it scopes the layer to that state, and is mutually exclusive with `govids` (400 if both are given — `govids` already names an exact cohort). |
| `type` | `state\|county\|city\|township` or `0`–`3` | Types 4/5 are out of scope. |
| `govid` | 12-char `canonical_govid` | e.g. Broward County = `121011212191`. |
| `basis` | `harmonized` \| `raw` | Default: **corpus default** (harmonized on a schema_version ≥ 5 corpus, raw on schema_version 4 — the API deliberately doesn't override this, so a not-yet-upgraded corpus keeps serving valid results instead of every default call failing; see "Harmonized basis & recipes"). Reported truthfully in `meta.basis`/`provenance.basis` either way. On `/governments/{govid}/spending`, `/revenue`, `/profile`, and the flat `/spending`, `/revenue` endpoints only. |
| `recipe` | Id from `GET /recipes` | Multi-code cross-vintage series; mutually exclusive with `category` and `subtype` → 400 if either is also set (a recipe result's subtype always reads `"recipe"`, so a subtype filter could only ever match nothing). Same endpoint scope as `basis`. |
| `expenditure_concept` | `primary` \| `direct` \| `total` | Default **`primary`** (spending on services, excluding debt service). `direct` adds interest on long-term debt (`I89`) and equals Census's published **Direct Expenditure**; `total` adds payments to other governments (`Total = Direct + M + L`) — see "Which `expenditure_concept` do I want?" above. **`total` is available on exactly one endpoint: `/governments/{govid}/spending`.** `/spending` (flat), `/rollups`, `/governments/{govid}/peer-comparison`, and `/governments/{govid}/profile` all accept the param but reject `total` with a 400 naming `/governments/{govid}/spending` as the fix (`primary` and `direct` still work on all four). **Every `/revenue` endpoint rejects the param outright with a 400 for ANY value** — expenditure concepts do not apply to revenue; use `revenue_concept` there instead. Reported truthfully in `meta.expenditure_concept`/`provenance.expenditure_concept` wherever it applies — never on a `/revenue` response; on `/governments/{govid}/spending`, `meta.expenditure_concept_direct_suppressed`/`provenance.expenditure_concept_direct_suppressed` are also reported — see "Provenance block" below. Orthogonal to `basis`; mutually exclusive with `recipe` when set to `total`. |
| `revenue_concept` | `general` \| `total` | Default **`general`** = Census General Revenue (`own_source` + `federal` + `state` + `local_aid`). `total` adds utility (`A91`–`A94`), liquor store (`A90`) and insurance trust revenue, per Census's identity `Total Revenue = General + Utility + Liquor Store + Insurance Trust` (classification manual §4.3) — see "Which `revenue_concept` do I want?" below. **Unlike expenditure `total`, revenue `total` is available on EVERY revenue endpoint** (`/revenue`, `/governments/{govid}/revenue`, and the revenue rows of `/governments/{govid}/profile`) — it adds the government's own receipts, not transfers received, so summing it across governments double-counts nothing. Passing it on a **spending** endpoint is a 400, the mirror of the rule above. Reported in `meta.revenue_concept`/`provenance.revenue_concept` on revenue responses, and absent on spending responses. |
| `limit`, `page` | integers | Pagination. `page` is a **zero-based page index**, not a row offset: page `N` returns rows `N*limit + 1` through `(N+1)*limit`, so consecutive pages never overlap and walking `page=0,1,2,…` until the response comes back empty reaches every row exactly once. E.g. at `limit=3`, `page=0` is rows 1-3 and `page=1` is rows 4-6. `limit` maximum is **1000** — a request above that is **rejected with a 400 naming the cap, not silently clamped down to it**; a clamp would look like a complete answer while under-reporting by whatever was cut. Use `page=` to walk further pages. `meta.page` echoes the page you asked for, and `meta.total` is always the full unpaginated count — so the page count is `ceiling(meta.total / meta.limit)`. Honoured on every listed endpoint **including `/rollups`**, whose default `limit` is 1000. |

**Unknown query parameters are rejected, not ignored.** Every route declares the
parameters it accepts; anything else returns **400** naming the offending key and
listing what that route does accept. The contract is per-route because the same
spelling can be right on one endpoint and wrong on another — singular `year` is
correct on `/governments/{govid}/peers` and a 400 everywhere else, where the
documented spelling is `years`. Previously an unrecognised key was dropped in
silence and the caller got HTTP 200 with a well-formed answer for the wrong
scope (`?name=` on `/governments` returned 608 nationwide rows; `?year=2022` on
`/rollups` returned rows tagged 2023).

### `complete` — recover wide-era census zeros (cog-api#22)

`complete=true|false` (default `false`) on `/spending`, `/revenue`,
`/governments/{govid}/spending` and `/governments/{govid}/revenue`.

By default a category a government did not report in a given year simply has
no row. With `complete=true` the requested (category × year) grid is **filled**
from the corpus `code_set` table, and every row carries a **`value_source`**:

| `value_source` | meaning | `amt_nominal` |
|---|---|---|
| `reported` | the corpus carries this cell | as published |
| `census_zero` | dense-source year (**≤ FY2011**), cell absent — Census published **$0** | `0` |
| `not_reported` | sparse-source year (**≥ FY2012**), cell absent — **unknown** | **`null`** |

**`not_reported` carries a JSON `null`, and it must not be read as zero.**
That is the whole point of the distinction. Filling a modern absence with `0`
invents data — it turns "we don't know" into "they spent nothing", which is a
different and much stronger claim. Only the pre-FY2012 era supports the zero
reading, because the source was dense there: an absent cell really did mean
Census published `$0`.

This matters more over HTTP than in R. An R user who gets an empty result can
inspect `cog_explain()` and reason about it; a Tableau or BI consumer gets an
empty cell and reads it as zero. The wide era is where most "why did this
series stop?" questions come from, and every one of them used to return
nothing with no explanation.

```
GET /api/v1/governments/532063205599/revenue?years=2011&complete=true
  -> reported rows, plus census_zero rows at amount 0

GET /api/v1/governments/532063205599/revenue?years=2020&complete=true
  -> reported rows, plus not_reported rows at amount null
```

**`meta.total` counts the FILLED rows.** Completion adds rows, and `meta.total`
is the count of what the query returned, so it grows accordingly — a
government-year that returns 8 rows bare may return 10 completed. Paging is
unaffected and pages over the completed set. `meta.complete` echoes the
parameter when you send it, so a response always states which contract its
`meta.total` is under.

**`provenance.completion`** describes what was filled:

| field | meaning |
|---|---|
| `applied` | whether completion ran |
| `rows_filled` | how many rows were added |
| `absence_means` | per year, which of `census_zero` / `not_reported` applies |

`absence_means` is keyed by year because a single request can span the seam:
`years=2011,2020` reports `census_zero` for 2011 and `not_reported` for 2020.

**Refused, rather than silently ignored, where it cannot work:**

| request | result |
|---|---|
| `complete` + `recipe` | **400** — a recipe defines its own component set; there is no grid to fill |
| `complete` + `expenditure_concept=total` | **400** — the intergovernmental leg has no code_set grid |
| `complete` on `/profile` | **400** — it stacks a spending and a revenue result into one table, so one `value_source` column and one `provenance.completion` cannot describe both grids honestly. Use the two single-flow routes |
| `complete` on `/balances`, `/rollups`, `/peer-comparison` | **400**, unknown parameter — holdings have no dense/sparse era distinction, and the aggregate routes have no per-government grid |

A silently dropped `complete=` would be especially quiet: the caller gets a
plausible, well-formed, *incomplete* series with nothing saying the completion
they asked for did not happen.
### Which subtypes exist — and which concept reveals each one

`subtype` is the **economic character** of a row (how the money behaves),
orthogonal to `category` (what function it serves). A previous version of this
table listed only three spending and four revenue values, which was wrong in
the way that matters most: it asserted completeness by omission, so a client
reading this document as the authority concluded the missing values could not
exist — then met them in ordinary responses with no documented meaning
(cog-api#44).

**Which values can appear depends on the concept you asked for.** That is the
part a bare enumeration cannot tell you, and its absence is its own trap:
filter `subtype=interest` under the default concept, get zero rows, and
conclude the government carries no debt service.

#### Spending subtypes

| subtype | meaning | appears under |
|---|---|---|
| `operations` | Salaries, supplies, contractual services — running the service | `primary` (default), `direct`, `total` |
| `capital` | Construction, land, equipment purchases | `primary` (default), `direct`, `total` |
| `assistance` | **Public-assistance payments to individuals** (the Census `J`-object rows on the harmonized basis) — cash assistance, vendor payments, other public welfare | `primary` (default), `direct`, `total` |
| `interest` | Interest on long-term debt (`I89`) | `direct`, `total` — **not** the default |
| `insurance_benefits` | Benefit payments out of insurance trust systems | `direct`, `total` — **not** the default |
| `intergovernmental` | Payments to *other* governments (`M`/`L` objects) | `total` only, and `total` is available only on `/governments/{govid}/spending` |

`assistance` is carried mainly by **states and counties**, which administer
public welfare; cities and townships generally do not report it. It is present
under the **default** concept, so it appears in ordinary `/spending` output
without any parameter being set — which is why omitting it from this table was
the most damaging of the six omissions.

#### Revenue subtypes

| subtype | meaning | appears under |
|---|---|---|
| `own_source` | Taxes and charges the government raises itself | `general` (default), `total` |
| `federal` | Intergovernmental revenue from the federal government | `general` (default), `total` |
| `state` | Intergovernmental revenue from a state government | `general` (default), `total` |
| `local_aid` | Intergovernmental revenue from other local governments | `general` (default), `total` |
| `utility` | Water, electric, gas, transit utility revenue | `revenue_concept=total` only |
| `liquor_store` | Liquor store revenue | `revenue_concept=total` only |
| `insurance_trust` | Insurance trust system revenue | `revenue_concept=total` only |

#### Balance subtypes

`general`, `employee_retirement`, `unemployment_trust`, `workers_comp_trust`,
`other_insurance_trust` — see [Cash and security holdings](#cash-and-security-holdings),
where `category` is a strict coarsening of `subtype` rather than an orthogonal
axis.

A subtype from the wrong flow is a **400**, never a silent empty result, and
so is a combination that can never match (`subtype=intergovernmental` without
`expenditure_concept=total`).

### Required parameters

Most parameters here are filters and every one of them is optional. Two
endpoints are different — one has no meaningful default for its geography and
returns nothing without you saying what you want; the other used to make the
same demand of `category`, but no longer does (`/rollups`, cog-api#37):

| Endpoint | Required | Omitting it returns |
|---|---|---|
| `GET /rollups` | `layer` (only) | `layer must be one of state, county, city.` |
| `GET /governments/{govid}/peer-comparison` | `category` | `category is required for peer comparison.` |

Both are **400**s, and each names the missing parameter, so the working call
is recoverable in one or two round trips. Stated here so it need not be.

Worked examples — the shortest call that returns rows on each:

```
GET /api/v1/rollups?layer=state&category=Police&years=2022
GET /api/v1/rollups?layer=county&category=Police&years=2022&state=WI
GET /api/v1/rollups?layer=state&years=2022
GET /api/v1/governments/552025209777/peer-comparison?category=Police&years=2022
```

The third example omits `category` entirely — `/rollups` is the one
money endpoint where that's the *point*, not an oversight: it returns the
all-categories total described next.

Note `category` is required *only on `/governments/{govid}/peer-comparison`*.
On `/spending`, `/revenue`, `/governments/{govid}/profile`, and now
`/rollups` too, it is an ordinary optional filter — omitting it there returns
every category (on `/rollups` specifically, see below for exactly what that
means).

### `category` is optional on `/rollups` — the `All Categories` total

Before cog-api#37, an unscoped geographic total cost one call per category
(26 round trips for the expenditure categories alone, then a client-side
sum). As of `uscogdata` ≥ 0.2.0, `/rollups` accepts an **omitted or explicit
`category=All Categories`** and returns one row per government **per
subtype** — spending summed across every expenditure category inside that
subtype — computed server-side rather than by whatever the caller happened
to add up. `All Categories` is a **reserved** category name, not an ordinary
entry from `/categories`. Passing a *revenue* or *balance* category (e.g.
`category=Property Tax`, `category=Fund Balances`) is rejected with a 400
naming the category's type — `/rollups` wraps an expenditure-only verb.

**`All Categories` is not the same thing as `expenditure_concept=total`.**
The two are orthogonal axes and combine freely:

- `expenditure_concept` (`primary`\|`direct`\|`total`) picks **which
  subtypes are in scope** — e.g. whether interest on long-term debt or
  intergovernmental transfers are included at all.
- `category=All Categories` picks **whether the rows inside that scope are
  broken out by category or summed** into one row per government per
  subtype.

So `category=All Categories&expenditure_concept=direct` sums every
expenditure category's *direct* figure; leaving both at their defaults sums
every category's *primary* figure. Neither parameter substitutes for the
other.

### Reporting coverage — how many governments are actually behind a number

The Census of Governments is a **complete census only in years ending in 2 and
7**. Every other year is a sample, and the sample varies enormously: Wisconsin's
608-city universe reports 597 governments in FY2012 and 112 in FY2019. A
statewide total resting on a fifth of the universe otherwise looks exactly like
one resting on all of it.

`/governments/{govid}/peer-comparison`, `/governments/{govid}/peers`, **and
`/rollups`** therefore report coverage on **`meta`**, not only in
`provenance`:

| field | meaning |
|---|---|
| `n_units_expected` | peer-comparison/peers: the cohort you were promised — the size of the nominal peer set. `/rollups`: the count of governments expected to report that year — a **per-year** number, since a rollup has no fixed cohort the way a peer comparison does |
| `n_units_reporting` | how many actually contributed rows |
| `is_census_year` | whether the year is a full census (ends in 2 or 7) — a statement about the **survey calendar**, never a claim of completeness |
| `coverage_by_year` | the per-year table, when several years are requested |

`is_census_year` being `true` does **not** mean everyone reported: FY1967 is a
census year in which only 97 of Wisconsin's 608 cities appear.
`n_units_reporting` is the number that tells the truth.

`provenance.scope.govids_missing` names the units that resolved but
contributed nothing for the requested year and category — e.g. a peer that
reported Police every year through FY2020 and then stopped. It is **not
peer-comparison-only** (this document previously said so, wrongly): every
route that takes an explicit govid set carries it, including the flat
`/spending` and `/revenue` with a `govid=` list, where it is the field that
distinguishes a stale id from a government that genuinely reported nothing.
On `/peer-comparison` a cohort member is always resolvable (it came from the
registry), so there it means only "contributed no rows". `/rollups` has no
fixed cohort for it to describe at all.

### `categories_missing` — a category that stops early (cog-api#56)

`govids_missing` answers "which of my **units** contributed nothing?".
`provenance.scope.categories_missing` answers the same question for
**categories**, which previously had no signal at all.

Governments routinely report some categories for longer than others. Spokane
City's property tax runs to FY2023 while its IG State stops at FY2021 and its
IG Local at FY2017 — so a per-category "% change across the window" silently
measures a **different window for each category**, and nothing in the
response said so. Detecting it meant counting rows against an expectation the
client had to construct itself.

Every **per-government** money route — `/governments/{govid}/spending`,
`/revenue`, `/balances` and `/profile` — now carries three fields under
`provenance.scope`:

| field | meaning |
|---|---|
| `years_present` | the years this result actually returned rows for |
| `categories_found` | every category that appeared at least once |
| `categories_missing` | `[{category, years}]` — categories present in **some** of `years_present` and absent in others |

```json
"scope": {
  "years_present":      [2020, 2021, 2022, 2023],
  "categories_found":   ["IG State", "Property Tax", ...],
  "categories_missing": [{"category": "IG State", "years": [2022, 2023]}]
}
```

`categories_missing` is an **empty list**, not an absent key, when every
category spans the whole series — so a consumer can read it unconditionally
whenever rows came back. `years` is always a JSON array, even for a
single-year gap.

**The reference set is `years_present`, not the years you requested.** If a
government reported nothing at all in one requested year, that is a fact
about the year rather than about any category, and diffing against requested
years would mark every category missing in it. A category absent throughout
never appears in `categories_found`, so this cannot flag a category a
government legitimately does not run.

**The fleet routes deliberately do not carry these fields.** "IG State is
missing in 2022" has no single truth value across a response spanning 20,000
governments — it is true for some and false for others, and the field carries
no govid to disambiguate. Ask a per-government route for a specific unit.

All three endpoints, **including `/rollups` (cog-api#36)**, also accept
**`coverage`**:

| value | effect |
|---|---|
| `all` (default) | every unit that reported that year — unchanged behaviour |
| `census` | census years only; a 400 if the requested range holds none |
| `consistent` | only units reporting in *every* requested year — a balanced panel |

`/rollups` carries `provenance.coverage_mode` plus a per-year
`provenance.coverage` table (`year`, `n_units_reporting`, `n_units_expected`,
`is_census_year`), and echoes those same numbers into `meta` — the fields
above. What it lacks relative to `/peer-comparison` is the **cohort**
framing: `n_units_expected` there is a fixed peer-set size, not a per-year
count. Nothing else.

The coverage fields are reported **regardless of mode**, so the default cannot
mislead silently either.

**`coverage=census` with no `years=` given resolves to the most recent
census year**, rather than erroring or filtering every row away
(`/governments/{govid}/peer-comparison` since cog-api#34, `/rollups` since
cog-api#36). Both endpoints default `years` to the corpus's latest year when
`coverage` isn't `census`; against a *sample* year (8 years out of 10),
`coverage=census` would otherwise filter that lone requested year away and
the query would die downstream with nothing left to return. An explicit
`years=` naming **only** non-census years under `coverage=census` is a
**400** naming the census years in the published window — not a silent empty
result, and not a 500. An explicit `years=` that **does** include a census
year is always honoured untouched, even under `coverage=census`.

### Peer-comparison `summary_*` rows are **not additive**

`/governments/{govid}/peer-comparison` returns the target's rows (`role`
`target`), one row per peer (`role` `peer`), and three cohort rows:
`summary_p25`, `summary_p50`, `summary_p75`.

Those three are computed **separately inside each `(year, subtype, category)`
cell**. A `summary_p50` row is therefore *the median peer's value in that one
category* — **not** the value of the median peer's total. The median peer for
Police and the median peer for Fire are usually different governments, so:

```
summary_p50(Police) + summary_p50(Fire)  ≠  the median peer's Police + Fire
```

This matters here more than it might elsewhere, because this endpoint
**requires `category` and accepts exactly one value** (`category=Police,Fire`
is a 400, not a CSV list). Anyone comparing across several categories must
therefore make one call per category and combine the results — which is
precisely where the naive sum happens. Facet or report the cells separately;
do not add them up.

Every `summary_*` row also carries this warning in its own `notes` field, so
it travels with an extract rather than living only here.

### Documentation links are host-relative

`GET /api/v1/` advertises four documentation links, and they do not all sit
under the same prefix:

| Field | Value | Resolves to |
|---|---|---|
| `llms` | `/api/v1/llms.txt` | under the versioned API prefix |
| `data_dictionary` | `/api/v1/data-dictionary` | under the versioned API prefix |
| `docs` | `/__docs__/` | **host-relative — server root, not under `/api/v1`** |
| `openapi` | `/openapi.json` | **host-relative — server root, not under `/api/v1`** |

All four are absolute paths, so resolve them against the **host**, not against
the API base URL. Appending `openapi.json` to a base URL that already ends in
`/api/v1` produces a versioned path that does not exist and returns **404** —
the interactive docs and the OpenAPI spec are served by the app itself rather
than by the versioned API, and deliberately so: they describe every version
rather than living inside one.

```
https://cog-api.civilytics.org/openapi.json    # 200
https://cog-api.civilytics.org/__docs__/       # 200
```

## Endpoints

| Route | Purpose |
|---|---|
| `GET /` , `GET /health` | Metadata root; liveness. |
| `GET /governments?q=&state=&type=` | Search / resolve → `canonical_govid`. |
| `GET /governments/{govid}` | Identity + lifespan. |
| `GET /governments/{govid}/profile` | ★ Full spending **and** revenue history (real + per-capita by default). Accepts `basis`/`recipe`. Accepts `expenditure_concept`, but rejects `total` (400) — blends spending and revenue into one table; use `/governments/{govid}/spending?expenditure_concept=total` instead. Also accepts `revenue_concept`, which applies to its revenue rows. |
| `GET /governments/{govid}/spending` , `/revenue` | Single-flow time series (filterable). Accepts `basis`/`recipe`. `/spending` also accepts `expenditure_concept` — **this is the only endpoint in the whole API where `total` is available.** `/revenue` rejects `expenditure_concept` outright (400, any value at all) and takes `revenue_concept` (`general`\|`total`) instead. |
| `GET /governments/{govid}/balances` | Cash and security holdings — a **stock**, not a flow. Accepts `basis`/`recipe`/`subtype`. Rejects **both** `expenditure_concept` and `revenue_concept` outright (400, any value at all) — concepts describe flows and have no meaning for a balance. See *Cash and security holdings* below. |
| `GET /governments/{govid}/peers` | Peer cohort (same type, similar population). |
| `GET /governments/{govid}/peer-comparison?category=` | Target vs. peers (per-capita). No `subtype`, no `basis`/`recipe` — ranks are cohort-wide and the underlying verb doesn't expose either. Accepts `expenditure_concept`, but rejects `total` (400) — it adds cohort-wide percentile summary rows (P25/median/P75) blended across every peer's own figure, not any one government's own spending. |
| `GET /spending` , `/revenue` | Flat nationwide slices (`govid` list or `state`+`type`). Accepts `basis`/`recipe`. `/spending` also accepts `expenditure_concept`, but rejects `total` (400) — this is a fleet endpoint (many governments) whose rows are meant to be summed, same as `/rollups`; use `/governments/{govid}/spending?expenditure_concept=total` for one government. `/revenue` rejects `expenditure_concept` outright (400, any value at all) and takes `revenue_concept` (`general`\|`total`) instead. |
| `GET /rollups?layer=state\|county\|city` | Geographic aggregates. `layer` is the only required parameter; `category` is optional (omitted → `All Categories`, see above) and a revenue/balance category is rejected (400). No `basis`/`recipe` (underlying verb doesn't expose either). Accepts `expenditure_concept`, but rejects `total` (400) — it returns one row per government in the layer, meant to be summed across the layer. Accepts `state` to scope the layer (mutually exclusive with `govids`), `coverage` (`all`\|`census`\|`consistent`; `census` with no `years=` resolves to the latest census year), and paginates via `limit`/`page` like every other listed route. Rows carry `fips_state` so the layer can also be filtered client-side. |
| `GET /categories?type=&pattern=` | Metric dictionary. |
| `GET /recipes?pattern=` | Harmonization recipe catalog (`recipe_id`, `label`, `n_components`, `year_min`, `year_max`). |
| `GET /examples` | ★ **One worked, fetchable URL per endpoint**, as JSON: `[{endpoint, description, path, example_url}]`. `example_url` is absolute, built from the requesting host. Generated against the **live corpus** — the govid and years come from the mounted registry, so no example can rot when the corpus window moves — and every URL is asserted to return `200` by the test suite. Exists because agent fetch tools are commonly gated to URLs that appeared **verbatim** in the conversation, so one request here bootstraps the whole API (cog-api#57). Takes no query parameters. |
| `GET /data-dictionary` , `/llms.txt` , `/openapi.json` | Docs. |

## Metric catalog

**Expenditure by function:** Police, Fire, Corrections, Judicial & Legal,
Highways, Public Welfare, Health & Hospitals, Parks & Recreation, Libraries,
Sewerage & Solid Waste, Housing & Community, General Government, Natural
Resources, Protective Inspection, Other Transportation, plus Education (K-12 /
Higher) and utilities (Water/Electric/Gas/Transit) for applicable types.

**Revenue by source:** Own-source → Taxes (Property, General Sales, Individual
Income, Corporate Income, Other), Current Charges, Miscellaneous;
Intergovernmental → Federal, State, Local aid.

Call `GET /categories` for the authoritative, machine-readable list (with
`category_type` and `subtype`).

## Row fields (monetary endpoints)

| Field | Meaning |
|---|---|
| `year` | Fiscal year. |
| `canonical_govid`, `gov_name` | Government identity. |
| `category` | Metric category (see catalog). |
| `spend_subtype` / `revenue_subtype` | Subtype on single-flow endpoints. `/profile` normalizes both to `subtype` and adds `flow` (`expenditure`/`revenue`). |
| `amt_nominal` | Amount in nominal USD. |
| `amt_real` | Inflation-adjusted USD (present when `adjust=real`). |
| `amt_per_capita_nominal` / `amt_per_capita_real` | Per-capita forms (present when `per_capita=true`). |
| `pop_source` | Population source for per-capita (`census_f33`). |
| `codes_included` | Census item codes summed into the row. |
| `aggregate_fallback`, `notes` | Provenance flags for cross-vintage handling. |
| `value_source` | **Only present under `complete=true`.** `reported` (the corpus carries this cell, amount as published), `census_zero` (dense-source year ≤ FY2011, cell absent — Census published $0, amount `0`), or `not_reported` (sparse-source year ≥ FY2012, cell absent — **unknown**, amount **`null`**). **A `not_reported` amount is `null` and MUST NOT be read as zero** — filling a modern absence with `0` turns "we don't know" into "they spent nothing". See [`complete`](#complete--recover-wide-era-census-zeros-cog-api22). |

> **Corpus schema v6 (2026-07-22, FIPS geography harmonization).** The API
> serves v6 corpora unchanged (its geography always came from the canonical
> xwalk). For anyone reading the published **raw long parquet** directly:
> `fips_state`/`fips_county` are now the government's **present/harmonized**
> geography (current county identity carried back to every year — e.g.
> Bridgeport CT is Greater Bridgeport `120` in every year), NOT the as-of-year
> value they held under v4/v5. As-of-year geography moved to
> `fips_state_asof`/`fips_county_asof` (renamed from `*_code`), and the raw
> GOVS source codes are preserved in `cog_legacy_state`/`cog_legacy_county`
> (populated FY2002–2016, `NA` FY2017+). 28 columns; see the corpus
> `docs/reader-specification.md` §3.

## Harmonized basis & recipes

Census item codes change meaning across vintages (functions get split,
merged, or retired). Two mechanisms make cross-vintage series comparable:

- **`basis=harmonized`** sums item codes through a curated 1:1 cross-vintage
  mapping (`harmonized_code`), folding series-break-affected codes onto a
  comparable target. Rows with no harmonized mapping (typically discontinued
  or not-yet-ruled codes) are **excluded** from the harmonized total;
  `provenance.harmonization` reports how many were dropped
  (`na_rows_excluded`, `na_amount_excluded`). `basis=raw` reproduces the
  plain published item codes with no folding or exclusion. **Leaving
  `basis` off entirely defers to the corpus**: harmonized on a
  schema_version ≥ 5 corpus, raw (with a `provenance.basis_note` explaining
  why) on an older schema_version 4 corpus — the API never substitutes its
  own default, so a request that doesn't mention `basis=` keeps working
  through a corpus upgrade instead of erroring. Passing an *explicit*
  `basis=harmonized` on a schema_version 4 corpus is a 400 (that combination
  genuinely isn't available there), not a silent downgrade. Always check
  `meta.basis` / `provenance.basis` for what was actually used — don't
  assume from what you requested.
- **`recipe=<id>`** covers series a 1:1 mapping can't express — e.g. a
  wide-era code that only splits into several leaf codes in the modern era.
  `GET /recipes` lists valid ids (`recipe_id`, `label`, `n_components`,
  `year_min`, `year_max`); pass one as `recipe=` instead of `category=` (the
  two are mutually exclusive — passing both is a 400). `subtype=` is also
  rejected alongside `recipe=` (400) rather than silently returning `[]`: a
  recipe result's `category` reads the recipe's label and its subtype column
  always reads `"recipe"`, so an `operations`/`capital`/`own_source`/...
  filter could never match a row. Because a recipe query bypasses the harmonized/raw view
  entirely, `provenance.basis` reads the literal string `"recipe"` (never
  `"harmonized"`/`"raw"`, regardless of any `basis=` also passed) with an
  inert `harmonization` block — see `provenance.recipe` for the component
  codes instead. **`/profile` + `recipe=`:** a recipe result is
  flow-agnostic by construction (it doesn't distinguish spending from
  revenue codes), so `/profile` queries it once and tags the combined rows
  `flow: "recipe"` rather than duplicating them under both `"expenditure"`
  and `"revenue"`.
- **`suggestions`** (top-level array on every response, `[]` when there's
  nothing to suggest): when a `basis=harmonized`, `category=`-scoped query
  comes back with no rows for a requested year *and* a cataloged recipe's
  component codes would actually cover that government in that year, the
  gap is signposted as `{recipe_id, label, available_years: [min, max],
  hint}` — a hint at the coverage the corrections-recipe family exists to
  fill (see `/recipes`). Scoped to `category`-filtered harmonized queries
  only; an unscoped query has no single coverage question to answer.

## Provenance block

Present on monetary responses (from `uscogdata`'s `cog_explain`). Records the
verb + call, the years/category queried, the unit conversion (`×1000`), the
per-capita denominator source and population years, the inflation base year and
index, and the corpus `manifest` (schema version, pipeline commit, build time).
`provenance.basis` is one of `"harmonized"`, `"raw"`, or `"recipe"` (the last
only when `recipe=` was used — see "Harmonized basis & recipes" above).
Note: `subtype` is an API-layer row filter, so `provenance` describes the full
(pre-subtype-filter) verb query — the applied subtype is reported in `meta.subtype`.

On `/governments/{govid}/spending` — the only endpoint where
`expenditure_concept=total` is available — `provenance` (and `meta`) also
carry:

- `expenditure_concept`: `"direct"` or `"total"`, whatever the verb actually
  resolved to (never just an echo of the request param).
- `expenditure_concept_note`: present (non-empty) only for `total`; explains
  `Total = Direct + intergovernmental` and how the legacy-era IG leg is
  assembled.
- `expenditure_concept_direct_suppressed`: boolean, `TRUE` when at least one
  requested (year, category) is a legacy (≤ FY2011) aggregate-only family
  whose Direct leg has **no rows at all** in this corpus — for those rows, a
  `total` figure is the intergovernmental leg **alone**, not Direct + IG.
  Mirrored into `meta.expenditure_concept_direct_suppressed` so a JSON-only
  consumer sees the flag without reading `provenance`. When `TRUE`, check
  each affected row's own `notes` (names the mechanism per row) and the
  top-level `suggestions` array (harmonization recipes — and, for an
  intergovernmental leg, that recipe's own IG-counterpart recipe — that
  recover the missing Direct component) before comparing the figure across
  years. See "Which `expenditure_concept` do I want?" above for the full
  legacy-years explanation.

## Cash and security holdings

`GET /governments/{govid}/balances` returns Census cash-and-security holdings:
fund balances, retirement system holdings and insurance trust balances.

**Holdings are a stock, not a flow.** Every other monetary endpoint reports
activity *over* a fiscal year. These rows report a balance *at a point in* time.
The two are not summable with each other, and Census's expenditure and revenue
concepts describe flows only — which is why this endpoint rejects both
`expenditure_concept` and `revenue_concept` outright, for any value.

**They are NOT GAAP fund balance.** Census holdings are **gross**: no
liabilities are netted. A reserve ratio built from them overstates what is
actually available. This is surfaced as `meta.not_gaap` (so a JSON-only
consumer sees it without reading `provenance`) and in full at
`provenance.balance_caveats`.

**`category` and `subtype`.** Unlike the money endpoints — where `category`
(function: Police, Fire) and `subtype` (economic character: operations,
capital) are genuinely orthogonal — the holdings taxonomy is a strict tree:

| `category` | `subtype` values |
|---|---|
| `Fund Balances` | `general` |
| `Insurance Trust Balances` | `unemployment_trust`, `workers_comp_trust`, `other_insurance_trust` |
| `Retirement System Holdings` | `employee_retirement` |

So `category=Fund Balances` **is** `subtype=general`. Use `subtype` when you
need one insurance-trust fund on its own, which `category` cannot express. An
impossible pair (`category=Fund Balances&subtype=unemployment_trust`) returns
**400**, never an empty result that reads as "this government holds none".

**Coverage windows.** The families do not span the same years, and
`provenance.balance_caveats.coverage_window` reports each one measured from the
corpus. `meta.coverage_truncated` names any family your requested span runs
past. Notably, employee-retirement holdings stop after **FY2016**, when Census
moved government-administered retirement systems into the separate Annual
Survey of Public Pensions — a collection-scope change, not a real one.

**`per_capita` means reserves per resident**, a stock per person. It is *not*
comparable to per-capita spending, which is a flow per person.