# US Census of Governments Finance API (v1) Base: https://cog-api.civilytics.org/api/v1/ Public, read-only JSON. Envelope: {status, data, error, meta, provenance, suggestions}. CANONICAL EXAMPLES -- one complete, working url per endpoint. These are here because many agent fetch tools will only retrieve a url that has appeared VERBATIM in the conversation: a documented pattern is not enough, the literal string has to exist somewhere first. Every url below is absolute and can be fetched as-is, so this file alone bootstraps the whole API. The same list is available as JSON, generated against the LIVE corpus rather than written by hand, at /api/v1/examples -- prefer it if you are enumerating programmatically: [{endpoint, description, path, example_url}], where example_url is absolute and every one of them is verified to return 200 by this API's own test suite (cog-api#57). https://cog-api.civilytics.org/api/v1/ https://cog-api.civilytics.org/api/v1/examples https://cog-api.civilytics.org/api/v1/health https://cog-api.civilytics.org/api/v1/governments?q=Spokane&state=WA&type=city https://cog-api.civilytics.org/api/v1/governments/532063205599 https://cog-api.civilytics.org/api/v1/governments/532063205599/profile?adjust=real&per_capita=true https://cog-api.civilytics.org/api/v1/governments/532063205599/spending?years=2012,2017,2022&category=Police https://cog-api.civilytics.org/api/v1/governments/532063205599/revenue?years=2022&revenue_concept=total https://cog-api.civilytics.org/api/v1/governments/532063205599/balances?years=2022 https://cog-api.civilytics.org/api/v1/governments/532063205599/peers?year=2022&n=10 https://cog-api.civilytics.org/api/v1/governments/532063205599/peer-comparison?category=Police&years=2022 https://cog-api.civilytics.org/api/v1/spending?govid=532063205599&years=2022 https://cog-api.civilytics.org/api/v1/spending?state=WA&type=city&years=2022&category=Police https://cog-api.civilytics.org/api/v1/revenue?govid=532063205599&years=2022 https://cog-api.civilytics.org/api/v1/rollups?layer=state&category=Police&years=2022 https://cog-api.civilytics.org/api/v1/categories?type=spending https://cog-api.civilytics.org/api/v1/recipes https://cog-api.civilytics.org/api/v1/data-dictionary https://cog-api.civilytics.org/api/v1/llms.txt And the two SERVER-ROOT surfaces, which are NOT under /api/v1 -- written out in full here because their relative form is the single most common 404: https://cog-api.civilytics.org/openapi.json https://cog-api.civilytics.org/__docs__/ COMPARING TWO GOVERNMENTS IN ONE CALL: govid= on the flat /spending and /revenue routes accepts a COMMA-SEPARATED LIST, so a head-to-head needs one request, not two. It is the fastest way to build a comparison table: https://cog-api.civilytics.org/api/v1/spending?govid=532063205599,531063176804&years=2022&category=Police Any id in the list that does not resolve is named in provenance.scope.govids_missing (see below) rather than failing the request, so a typo in one of ten ids still returns the other nine. BULK DATA -- read this before writing a paging loop. Do NOT walk 1000-row JSON pages to assemble a dataset; there are two purpose-built paths, and the right one depends on how much you want: 1. THE WHOLE CORPUS (or any large slice of it): do not use this API at all. The corpus is published as hive-partitioned parquet, CC-BY, at https://huggingface.co/datasets/civilytics/us-cog-finance Read it directly -- it is faster than any number of API calls, costs the server nothing, and gives you every column: -- DuckDB (SQL, no download): 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 arrow::open_dataset("hf://datasets/civilytics/us-cog-finance/data/long") Note the corpus is the RAW long table: the API's harmonization, inflation adjustment, per-capita scaling and provenance are computed by the uscogdata package on top of it. Use the R package uscogdata against the mirror if you want those semantics offline. 2. MORE THAN A PAGE, LESS THAN THE CORPUS (e.g. every city in several states for one year): GET /spending or /revenue with format=csv, which accepts limit up to 50000 in ONE request: GET /spending?state=WI&type=city&years=2022&format=csv&limit=50000 Returns CSV rows instead of the JSON envelope; the metadata moves to response headers X-Total-Count, X-Limit, X-Page. The raised cap applies only when subtype= and recipe= are unset (those force post-query filtering, where the cap stays 1000). format= is accepted on /spending and /revenue only. START HERE -- resolving a place name to a govid. Every per-government route takes a 12-char canonical_govid in the path, and q= on /governments is what turns a name into one: GET /governments?q=Madison&state=WI&type=city -> exactly one row q= matches gov_name as a CASE-INSENSITIVE SUBSTRING (not anchored, not an exact match), ordered by ACS population descending, so a short q= can return many rows -- narrow with state= and type=. Caveat: q= 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. Omit q= to list every government matching the other filters. An empty result means ONE thing: status "success" with data [] means the government reported nothing matching your filters -- a real, common condition here. It never means a bad govid and never means an unavailable year; those are 400s with their own messages: malformed govid -> "govid '' is not a canonical_govid. Expected 12 digits ..." A canonical_govid is DIGITS ONLY and exactly 12 of them. Anything else -- a URL, a name, a truncated or FIPS-style id -- is refused on every route, including the flat govid= list (cog-api#55). Before this it returned an empty success, so a mis-pasted id and a government that genuinely reported nothing were the same response. unknown government -> "Unknown govid ''." (on EVERY /governments/{govid}/... route, not just the identity route). NOTE this is the WELL-FORMED but absent case, and it behaves differently on the flat routes: there, a well-formed unknown id is NOT an error -- it returns success and is named in provenance.scope.govids_missing, so a ten-id list does not fail because one entry went stale. An empty result on a PATH-govid route means ONE thing: status "success" with data [] means the government reported nothing matching your filters -- a real, common condition here. On those routes it never means a bad govid and never means an unavailable year; those are 400s with their own messages: unknown government -> "Unknown govid ''." (on EVERY /governments/{govid}/... route, not just the identity route) THE FLAT ROUTES ARE THE EXCEPTION, and it is the easiest mistake to make against this API: /spending?govid=... and /revenue?govid=... take a LIST, so one unresolvable id must not fail the other nine. An id that is unknown -- or not even an id, e.g. a url pasted into govid= by accident -- is therefore reported in provenance.scope.govids_missing under status "success" with data [], NOT as a 400. Empty-plus-success on a flat route is only a genuine reporting gap once govids_missing is empty. Check it before concluding a government reported nothing. year out of range -> "year must be in -." year in range but no source data -> "year is inside the published range - but has no source data. ..." The published years are a SET, not a span -- it can have holes, and GET / names them in its scope string from the same source as that error. Required parameters (everything else is an optional filter): /rollups requires layer= (only -- see below) /governments/{govid}/peer-comparison requires category= layer= is REQUIRED on /rollups and accepts exactly one of: state, county, city (no default). Omitting it is a 400 that names it -- "layer must be one of state, county, city." Omitting category= on peer-comparison is a 400 too -- "category is required for peer comparison." Worked examples: GET /rollups?layer=state&category=Police&years=2022 GET /rollups?layer=county&category=Police&years=2022&state=WI GET /rollups?layer=state&years=2022 GET /governments/552025209777/peer-comparison?category=Police&years=2022 category= is required ONLY on peer-comparison; on /spending, /revenue and /governments/{govid}/profile it is an ordinary optional filter. On /rollups category= is now OPTIONAL (cog-api#37, needs uscogdata>=0.2.0): omitting it returns an all-categories total, one row per government per subtype, under the reserved category "All Categories" (see below). A revenue or balance category (category=Property%20Tax, category=Fund%20Balances) is rejected on /rollups with a 400 naming its type -- /rollups serves expenditure categories only. COMPLETE= RECOVERS WIDE-ERA CENSUS ZEROS (cog-api#22). By default a category a government did not report in a year simply has no row. complete=true fills the requested (category x year) grid from the corpus code_set, on /spending, /revenue, /governments/{govid}/spending and /governments/{govid}/revenue. Every row then carries value_source: reported the corpus carries this cell amount as published census_zero dense-source year (<= FY2011) amount 0 not_reported sparse-source year (>= FY2012) amount null A not_reported amount is NULL AND MUST NOT BE READ AS ZERO. That is the entire point of the distinction: filling a modern absence with 0 turns "we do not know" into "they spent nothing", a different and much stronger claim. Only the pre-FY2012 era supports the zero reading, because the source was dense there and an absent cell really did mean Census published $0. GET /governments/532063205599/revenue?years=2011&complete=true -> reported rows plus census_zero rows at amount 0 GET /governments/532063205599/revenue?years=2020&complete=true -> reported rows plus not_reported rows at amount null meta.total counts the FILLED rows, so it grows when completion adds any; meta.complete echoes the parameter so a response states which contract its count is under. provenance.completion reports {applied, rows_filled, absence_means}, with absence_means keyed BY YEAR because one request can span the seam (years=2011,2020 reports census_zero for 2011 and not_reported for 2020). REFUSED with a 400, never silently ignored: alongside recipe= (a recipe defines its own component set, so there is no grid to fill), alongside expenditure_concept=total, and on /profile (it stacks spending and revenue into one table, so one value_source column cannot describe both grids). /balances, /rollups and /peer-comparison reject it as an unknown parameter. A CATEGORY ONLY WORKS ON THE ROUTE THAT SERVES ITS category_type (cog-api#41). Every money route 400s a wrong-flow category rather than returning an empty 200. Which route serves which type: expenditure /spending, /rollups, /governments/{govid}/spending revenue /revenue, /governments/{govid}/revenue balance /governments/{govid}/balances GET /spending?state=WI&years=2012&category=Property%20Tax -> 400 "category 'Property Tax' is a revenue category; /api/v1/spending serves expenditure categories only. Use /api/v1/revenue for a revenue slice, or see /api/v1/categories for each category's category_type." Do NOT read a 400 here as "no data" -- it means you asked the wrong route, and the message names the right one. /categories reports category_type for every category, so you can route correctly without guessing. TWO EXEMPTIONS. "All Categories" works on every money route whatever the flow (it is a reserved value, not a crosswalk entry -- see below). And /governments/{govid}/profile is ungated on purpose: it blends spending and revenue into one table, so both flows are legitimate there and a revenue category returns revenue rows exactly as before. SUBTYPE VOCABULARY, and which concept reveals each value (cog-api#44). subtype is the ECONOMIC CHARACTER of a row, orthogonal to category (the function it serves). Which values can appear DEPENDS ON THE CONCEPT you asked for, so a bare list is not enough to use it safely: SPENDING operations primary (default), direct, total capital primary (default), direct, total assistance primary (default), direct, total interest direct, total -- NOT the default insurance_benefits direct, total -- NOT the default intergovernmental total only, and total exists only on /governments/{govid}/spending REVENUE own_source federal state local_aid general (default), total utility liquor_store insurance_trust revenue_concept=total only BALANCES general employee_retirement unemployment_trust workers_comp_trust other_insurance_trust assistance is public-assistance payments to individuals (Census J objects) and is carried mainly by STATES AND COUNTIES; cities and townships generally do not report it. It IS in the default concept, so it shows up in ordinary /spending output with no parameter set. DO NOT read zero rows for a subtype as "this government has none" until you have checked the concept column above: subtype=interest under the default primary concept is always empty, because interest only enters at direct. "All Categories" is a RESERVED category, not an ordinary crosswalk entry -- it is NOT the same axis as expenditure_concept. expenditure_concept (primary|direct|total) picks WHICH SUBTYPES are in scope; category="All Categories" picks whether the rows inside that scope are broken out by category or summed into one row per government/subtype. The two combine freely, e.g. omitting category (All Categories) together with expenditure_concept=direct. Reporting coverage: the Census of Governments is a COMPLETE CENSUS only in years ending in 2 or 7; every other year is a sample, and the sample varies enormously (Wisconsin's 608-city universe: 597 reporting in FY2012, 112 in FY2019). /governments/{govid}/peer-comparison, /peers AND /rollups all report on meta: n_units_expected the cohort you were promised (peer-comparison/peers); on /rollups, the count expected to report that year -- a per-year number, since a rollup has no fixed cohort n_units_reporting how many actually contributed rows is_census_year whether the year is a full census -- a statement about the SURVEY CALENDAR, not a claim of completeness (FY1967 is a census year with 97 of 608 reporting) coverage_by_year per-year table when several years are requested provenance.scope.govids_missing names cohort members that resolved but contributed nothing for the requested year+category. It is NOT peer-comparison-only (this file previously said so, wrongly): every route that takes an explicit govid set carries it, including the flat /spending and /revenue with govid=. CHECK IT BEFORE READING AN EMPTY RESULT AS A REPORTING GAP -- an id that is malformed or simply unknown comes back in govids_missing with status "success" and no error, so the two cases look identical until you look here: provenance.scope.govids_found the ids that resolved provenance.scope.govids_missing the ids that did not /rollups is the exception: it has no fixed cohort for this field to describe. /peers, /peer-comparison AND /rollups (cog-api#36) all accept coverage= all|census|consistent (all = default, unchanged behaviour; census = census years only, 400 if the range holds none; consistent = only units reporting in EVERY requested year). /rollups carries provenance.coverage_mode plus a per-year provenance.coverage table (year, n_units_reporting, n_units_expected, is_census_year), and echoes the 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 (peer-comparison and rollups both do this, cog-api#34/#36) -- a default sample year under coverage=census would otherwise remove every requested year and die downstream. 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 or a 500. An explicit years= that DOES include a census year is always honoured untouched, even under coverage=census. A CATEGORY THAT STOPS EARLY IS NAMED (cog-api#56). 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 IG Local at FY2017. Nothing used to flag that, so a per-category "% change over the window" silently measured a DIFFERENT window for each category. Every per-government money route (/governments/{govid}/spending, /revenue, /balances, /profile) now carries, in provenance.scope: years_present the years this result actually returned rows for categories_found every category that appeared at least once categories_missing [{category, years}] -- categories that appeared in SOME of years_present and not others Read categories_missing BEFORE computing any per-category change over time. An empty list means every category returned spans the whole series; the key is always present when there are rows, so you can read it unconditionally. The reference set is years_present, NOT the years you asked for. That is deliberate: if a government reported nothing at all in one requested year, that is a fact about the year, not about any category, and diffing against requested years would mark every category missing in it. A category absent throughout never appears in categories_found at all, so this cannot flag a category a government legitimately does not run. The FLEET routes (flat /spending, /revenue, /rollups) do NOT carry these fields, and their absence is not an omission: "IG State is missing in 2022" has no single truth value across a response spanning 20,000 governments. Ask a per-government route if you need this for a specific unit. peer-comparison summary_* rows are NOT ADDITIVE. The response carries 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 WITHIN EACH (year, subtype, category) cell, so summary_p50 is the median peer's value in THAT ONE CATEGORY, not the value of the median peer's total -- the median peer for Police and for Fire are usually different governments. Since this endpoint requires category= and takes exactly ONE value (category=Police,Fire is a 400, not a CSV list), a multi-category comparison means N calls plus a combine step -- do NOT add the summary_* rows together across those calls; facet them. Each summary_* row repeats this warning in its own notes field. Docs links from GET /: llms and data_dictionary sit under /api/v1/, but docs (/__docs__/) and openapi (/openapi.json) are HOST-RELATIVE -- server root, NOT under /api/v1. Resolve them against the host, not against the base URL above; appending openapi.json to a base ending in /api/v1 is a 404. They describe every version rather than living inside one. Endpoints: /health, /, /governments, /governments/{govid}/profile, /spending, /revenue, /governments/{govid}/balances, /rollups, /categories, /recipes. Money params: adjust=nominal|real, base_year=YYYY, per_capita=true|false, basis=harmonized|raw, recipe=, expenditure_concept=primary|direct|total (default primary; spending-only; total available on exactly one endpoint -- see below), revenue_concept=general|total (default general; revenue-only; available on EVERY revenue endpoint -- see below). Unknown query parameters are REJECTED with 400 naming the offending key -- they are never silently ignored. The contract is per-route: singular year= is valid only on /governments/{govid}/peers; everywhere else the spelling is years=. /rollups also accepts state= (scopes the layer; mutually exclusive with govids=) and paginates via limit=/page= like every other listed route; its rows carry fips_state. years= (cog-api#38) accepts a single year (years=2022), a comma-separated list (years=2012,2017,2022), an inclusive range (years=2000-2010), or a mix (years=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 and /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 if the corpus has no source data for it that is still the specific "no source data" 400 above, even though the year 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, silently dropping the holes rather than inventing rows for them. A range 400s only when NO published year falls anywhere inside its span (a genuinely empty result, not a merely partial one). Pagination: page= is a ZERO-BASED PAGE INDEX, not a row offset. Page N returns rows N*limit+1 .. (N+1)*limit, so consecutive pages never overlap: at limit=3, page=0 is rows 1-3 and page=1 is rows 4-6. To extract everything, walk page=0,1,2,... until data comes back empty -- that reaches every row exactly once. meta.page echoes the page you requested; meta.total is the full unpaginated count, so the page count is ceil(meta.total/meta.limit). limit= maximum is 1000 on every listed route (cog-api#39). A request above that is REJECTED with a 400 naming the cap -- NOT silently clamped down to it, which would look like a complete answer while under-reporting by however much was cut. Use page= to walk further pages instead. BREAKING CHANGE 2026-07-31, silent (no error, no response flag): (1) revenue default narrowed to Census General Revenue -- utility/liquor/insurance-trust revenue LEFT the default, 15.9% of city revenue; pass revenue_concept=total to reproduce the old default exactly. (2) expenditure_concept=direct now INCLUDES interest on long-term debt, so an explicit direct request returns a larger figure than before; the DEFAULT figure is unchanged (the new default primary == the old default direct). Pass expenditure_concept=primary to keep previous numbers under an explicit request. BREAKING CHANGE 2026-08-05, status-code change (previously a silent 200): (3) limit= above 1000 (cog-api#39) is now REJECTED with a 400 naming the cap -- previously silently clamped to 1000 rows under status:"success" with nothing indicating rows were dropped; use page= to walk further pages instead. (4) /rollups?category= (cog-api#35) is now a 400 naming the category's actual type -- previously resolved against /categories and matched zero rows, a silent empty success; use /revenue (with state=/type=) or /governments/{govid}/balances for that data instead. BREAKING CHANGE 2026-08-13, status-code change (previously a silent 200): (5) a wrong-flow category= on ANY money route (cog-api#41) is now a 400 naming the category's type and the route that serves it. This extends (4) above from /rollups to flat /spending and /revenue, both per-government money routes, and /balances -- previously those returned status:"success" with data: [], so /spending?category=Property%20Tax read as "nobody reported property tax". "All Categories" and /profile are exempt. expenditure_concept: THREE concepts, nested smallest to largest. What each one INCLUDES and EXCLUDES, in plain language: primary (DEFAULT) = what the government spent running services -- salaries, supplies, construction, equipment. EXCLUDES interest on long-term debt, and excludes payments to other governments. direct = primary PLUS interest on long-term debt (item code I89). This is Census's own published "Direct Expenditure" concept, so use direct when reconciling against a Census table of that name. It still EXCLUDES payments to other governments. total = direct PLUS payments to other governments (Total = Direct + M + L). The only difference between primary and direct is debt service: a government that pays interest on bonds reports it under direct but not under primary. For Madison WI FY2020 that gap is $27,704,000 -- direct $651,051,000 vs primary $623,347,000 (4.4%). If your figure must tie to a published Census "Direct Expenditure" number, ask for direct explicitly; the default will be lower. total is available ONLY on /governments/{govid}/spending (govid is a path param, so it's always exactly one government). Every other SPENDING endpoint -- the flat /spending, /rollups, /governments/{govid}/peer-comparison, and /governments/{govid}/profile -- accepts primary and direct only; passing total there is a 400 naming /governments/{govid}/spending as the fix, not a silent downgrade. Every /revenue route rejects expenditure_concept OUTRIGHT for ANY value, with its own 400: expenditure concepts do not apply to revenue, which has its own pair -- see revenue_concept below. revenue_concept: TWO concepts, Census's own published pair, related by the identity in classification manual section 4.3: Total Revenue = General + Utility + Liquor Store + Insurance Trust general (DEFAULT) = Census General Revenue: own_source + federal + state + local_aid. EXCLUDES utility revenue (water/electric/gas/transit charges, A91-A94), liquor store revenue (A90), and insurance trust revenue (employee retirement and social insurance). READ THE NEXT SECTION BEFORE TRUSTING THAT SUM: for counties, cities and townships the federal/state/local_aid legs do NOT come back on a default query, so "general" in practice returns own-source only. total = general PLUS all three of those. This matters most for CITIES, which commonly run utilities: across the corpus, utility + liquor store revenue is 15.9% of city revenue, vs 4.8% townships, 1.7% counties, 1.2% states. A city's general revenue is therefore materially smaller than its total revenue -- for Madison WI FY2020, general $564,322,000 vs total $616,551,000. Unlike expenditure total, revenue total is available on EVERY revenue endpoint with no route restriction. The reason the two differ: expenditure total adds money handed to OTHER governments, which those governments report again as their own spending, so summing it across governments double-counts. Revenue total adds the government's OWN utility, liquor store and insurance trust receipts -- not transfers received from anyone -- so summing it across governments double-counts nothing. SERIES SEAM: the employee-retirement codes inside insurance trust (X01/X02/X05/X08) stop after FY2016, when those systems moved out of the annual finance file into a separate collection. A revenue_concept=total series therefore STEPS DOWN at FY2016/FY2017 for collection-scope reasons, not because revenue fell. Under the general default this seam does not arise at all, since insurance trust is excluded either way. Passing revenue_concept on a SPENDING endpoint is a 400, the mirror of the rule above -- it is never silently ignored. INTERGOVERNMENTAL REVENUE FOR LOCAL GOVERNMENTS NEEDS A recipe=. This is the single most misread behaviour in this API, so it gets its own section. Counties, cities and townships (~35,000 governments) report federal, state and local aid ONLY at the aggregate item codes B89 / C89 / D89. Aggregate rows are excluded from the ordinary category views by design, so: GET /governments/532063205599/revenue?years=2023 -> Property Tax, General Sales Tax, Other Taxes, Miscellaneous Revenue, Current Charges ... and NO IG Federal / IG State / IG Local rows. That is not a reporting gap and it is not a bug -- a city with zero federal and state aid is not a credible figure, and the money is there. Only the 50 STATES report the functional leaf codes (C21 education aid, C30, C42 ...) that flow through the normal path. Recover it with a recipe. USE THE TOTAL ONES -- one per aid stream, each a complete, continuous series from FY1967 to the end of the corpus: ig_federal_total_wide ig_state_total_wide ig_local_total_wide https://cog-api.civilytics.org/api/v1/governments/532063205599/revenue?years=2023&recipe=ig_state_total_wide These are self-contained: do NOT also add the category rows, and do not sum two of them together for one stream. They already carry the wide-era aggregate and every modern leaf, switching between them at the FY2012 boundary, so they reproduce the true total in every published year. The older residual recipes still exist and still work: ig_federal_b89_wide ig_state_c89_wide ig_local_d89_wide ig_federal_b47_wide ig_state_c47_wide ig_local_d47_wide but they are the ?89 / ?47 FAMILY only, not the total, and the ?89 ones change meaning at FY2022 (see the SERIES SEAM note below). Prefer the total recipes unless you specifically want the residual concept. The API TELLS YOU THIS, but only if you scope the query. Asking for the category by name returns zero rows plus a suggestion naming the recipe and the exact dollars being withheld: GET /governments/532063205599/revenue?years=2023&category=IG%20State data: [] suggestions: [{recipe_id: "ig_state_c89_wide", available_years: [1967, 2023], suppressed_amount: 72307000}] An UNSCOPED revenue query (no category=) emits NO suggestion -- a deliberate design decision, and the one shape where this stays silent. If you are charting revenue by category, you are making exactly that call: add the recipes yourself rather than waiting to be told. SERIES SEAM at FY2022 -- read this before comparing aid across it. The two consequences below are shown on the state (C) leg; for a LOCAL government they apply to the federal and local legs too (see the breakdown after). From FY2022 the Census source stops publishing intergovernmental detail: FY2021 C: C21 C30 C42 C46 C50 C79 C80 C89 C91 C92 C93 C94 FY2022 C: C89 (FY2023, FY2024 identical) Two consequences, and the second is the trap: 1. NO PER-FUNCTION STATE AID SERIES CONTINUES PAST FY2021. State aid for education (C21, $424.9B in FY2021 -- 71.7% of all state aid) is no longer separable from highway or health aid. This is not recoverable; the source does not carry it. 93.1% of state IG revenue lost its functional attribution at this seam. 2. C89 CHANGED MEANING, silently and without a gap in the numbers. It was the residual "other" category (6.9% of state aid in FY2021); from FY2022 it is the TOTAL. So a bare C89 series steps up ~6x at FY2022 for definitional reasons. Modern C89 should be compared against the pre-FY2022 sum of ALL C codes, not against pre-FY2022 C89: on that basis FY2022 is +7.2% over FY2021, in line with the +4.0/+1.8/+4.6 of the preceding years, rather than +518%. ig_state_total_wide already handles this -- it is the all-C total in every year, so it crosses the seam cleanly. That is the reason to prefer it. WHICH LEGS LOST DETAIL DEPENDS ON WHO REPORTS, and the local governments this section is about lost the most. Measured FY2021 vs FY2023: THE 50 STATES: only the C leg collapsed. The federal (B) and local (D) legs kept every functional code and dropped only the ?91-?94 sub-codes (CA, NY, TX, FL, IL all keep B21 B42 B46 B50 B79 B80 / D21 D42 D46 D79). COUNTIES, CITIES, TOWNSHIPS: ALL THREE legs collapsed to the aggregate. Seattle City went B46,B50,B79,B80,B89,B91 / C30,C46,C50,C79,C80,C89, C91,C92 / D46,D50,D79,D89 -> B89 / C89 / D89. Phoenix and Denver likewise. So for a local government NO aid stream carries functional detail after FY2021, and the three ig_*_total_wide recipes are the whole of what is recoverable. Catalogued as cog_pipeline SB307-SB326. Why (flat /spending, /rollups, /peer-comparison reject total specifically): each returns rows spanning multiple governments (a govid list/state filter, a geographic layer, or a peer cohort) that are meant to be summed or compared, and Total double-counts intergovernmental transfers when summed across governments (a state's M44 grant to a county is also the county's own E44/F44 direct spend); /peer-comparison specifically also blends every peer's figure into cohort-wide percentile rows (P25/median/P75), not a sum, but the same "spans more than one government -> direct only" rule applies. Why (/profile rejects total): unrelated reason -- it blends spending and revenue into one table, and expenditure total is a spending-only concept. /profile does accept primary, direct, and revenue_concept. Measured across every state x year county rollup in the published corpus (2,640 rollups, 1967-2023, 48 states): summing total overstates with a median of 3.3% -- treat that as typical, not a ceiling, since p95 is 26.8% and the max is 84.8% (Montana 1971). Legacy years: some (year, category) pairs have NO Direct rows at all (a pre-2012 aggregate-only family), so a total query there returns the intergovernmental leg alone, not Direct+IG. Flagged via provenance.expenditure_concept_direct_suppressed (mirrored into meta of the same name) plus per-row notes and top-level suggestions (recipes that recover the Direct component) -- check these before comparing a total figure across years. subtype=intergovernmental has rows only under expenditure_concept=total; requesting it otherwise (or on an endpoint where total isn't offered at all) is a 400, not an empty result. Example: /governments/121011212191/profile?adjust=real&per_capita=true Cite: Civilytics Consulting, US Census of Governments finance corpus.