canopy-reporting API Reference

On this page

Overview

Cross-link: canopy-reporting Data Model (#419)

Assembles federal SNAP/TANF/Medicaid reports from upstream service data. Does not access program databases directly (ADR-001 compliant) — instead queries canopy-persons, canopy-applications, canopy-snap, canopy-enrollment, canopy-renewals, canopy-tanf, canopy-medicaid, and canopy-caps via HTTP.

Base URL

http://localhost:8011/v1

Authentication

Bearer token (Keycloak RS256 JWT)

Minimum role

supervisor

Swagger UI

http://localhost:8011/swagger-ui

Database

canopy_reporting

Receiver contract (OIDC S-reporting, #1438 / ADR-043 §C)

canopy-reporting is the fourteenth service on the ADR-043 receiver contract (canopy_auth::ReceiverContract) — and the first whose dominant route class is USER-ONLY:

  • ALL 21 supervisor report surfaces are require_user_only(SUPERVISOR_OR_ABOVE): service bearers are 403 (service_class_on_user_only); under enforcement a broad-audience direct worker bearer is 403 (aud_not_exact), and only the exchanged user-context arm (exact aud=canopy-reporting, allowlisted azp, supervisor role) reaches them. Workers reach these surfaces via token exchange — there is no BFF sender today; the devstack suites ride jane.doe’s exchanged bearer.

  • The three DUAL surfaces keep their service arms by design: the overpayments summary (the dashboard panel’s service-class sender) and the two org-visible runs reads (authorize_runs_read — do not requester-scope).

  • Azp allowlist: canopy-web-exchanger only.

Reporting-Access Audit Stream (#1404, Pub 1075 §9)

Every successful report generation, report/summary read, and CSV extract on this surface stages one audit envelope through the ADR-018 outbox (ingested by canopy-security’s wildcard subscriber) via a route-table middleware (src/audit.rs) — a new route cannot land unclassified: an exhaustiveness test forces every OpenAPI (method, path) into the audited table or the explicit exclusion list. Vocabulary: reporting.report.generated (POST generate), reporting.report.accessed (report/summary reads), reporting.extract.exported (CSV extracts — the parsed export action also counts toward the seeded Bulk Data Access rule). Payload carries report_family, accessed_by (the caller’s sub) and user_role; the envelope asserts the family’s #1519 program stamp (FNS-388/QC → snap, ACF-/WPR → tanf, T-MSIS/CMS- → medicaid; the cross-program overpayment surfaces assert neutral). Fail-closed: if staging fails the response is withheld (500) — the disclosure has not crossed the wire, and a monitoring stream that drops silently is not a control. Deliberately NOT audited (recorded in UNAUDITED_ROUTES with the ruling): the run-status poll targets (GET /v1/reporting/runs, GET /v1/reporting/runs/{id}) — run METADATA, not report content, and auditing a dashboard poller would trip the 50/10min volume rule on routine traffic. This is the emitter half of the #1302-seeded "Reporting Extract Volume" detection rule.

Provisioning (both required — the live e2e test surfaced each): the 20261129000000 migration grants the app DB role the outbox INSERT the #1456 least-privilege matrix deferred, and the broker user needs the topic-write pattern in devstack/rabbitmq/definitions.json. The definitions import only runs against a FRESH broker volume; on a running devstack apply it live:

docker exec canopy-rabbitmq-1 rabbitmqctl set_topic_permissions -p / \
  canopy-reporting canopy.events \
  '^(reporting\.extract\.exported|reporting\.report\.accessed|reporting\.report\.generated)$' '.*'

Without the broker grant, staging still succeeds (fail-closed covers the INSERT, not the drainer) and events silt in the outbox with ACCESS_REFUSED retries — watch the outbox-backlog alert. The mq-topology lint cannot see these publish sites (#1530).

FNS-388 Monthly Participation Report

POST /v1/reporting/snap/fns-388

Enqueue a durable report run that generates the FNS-388 monthly report for a given month (#1202/#1203 MR5 — the run assembles active certifications, issuance totals, household counts, person demographics, and certification types from upstream services off the request path).

Request:

{
  "report_month": "2026-03-01"
}

Any day of the month is accepted; the period is canonicalized to the month start.

Response (202): ReportRunAcceptedrun_id, generation_id, poll_url — plus a Location header pointing at the poll URL. Poll GET /v1/reporting/runs/{id} until done, then read the published report from the GET endpoints below.

Response (409): the target month already has a queued/running run — the body carries THAT run’s ReportRunAccepted handle to poll.

Response (503): report runs disabled (RUNS_ENABLED=false) or the run queue at capacity — retry later.

The old synchronous 201 SnapMonthlyReport body is gone (pre-1.0, no shim); the type remains the GET response below.

Regulatory basis: 7 CFR 272.11 — monthly FNS-388 reporting.

GET /v1/reporting/snap/fns-388

List FNS-388 reports — published generations only; each item carries its generation’s provenance (see "Report provenance" below).

GET /v1/reporting/snap/fns-388/{month}

Get the report for a specific month (format: YYYY-MM), from the month’s published generation.

Response (200): SnapMonthlyReport, carrying its generation’s provenance.

Response (404): no published generation for the month (or no row under it).

FNS-7176 Quality Control Universe

POST /v1/reporting/snap/fns-388/{month}/submission

Advance the month’s FNS-388 submission lifecycle (#1335): draft → final → submitted → accepted | rejected, with rejected → final reopening resubmission. Acts on the month’s PUBLISHED generation’s row. Guard: user-only, supervisor+. Audited as reporting.report.submission_transitioned.

Request: SubmissionTransitionRequesttarget_status (final | submitted | accepted | rejected) and fns_confirmation_number (required on accepted, rejected with 400 on any other target).

Semantics: submitted stamps submitted_at (each submitted edge — a resubmission is a new filing, so the stamp reflects the CURRENT submission) and makes the filing IMMUTABLE to the #1202 promotion guard — a rerun for the month executes but its promotion refuses (immutable_target), so a shipped federal filing is never silently replaced; accepted stores the FNS confirmation and keeps the block. Transitions are compare-and-set on the row’s current status: an illegal edge is 409 (invalid_submission_transition), and a concurrent transition racing this one is 409 (submission_transition_raced — re-read and re-decide).

Response (200): the transitioned SnapMonthlyReport with provenance.

POST /v1/reporting/snap/qc-universe

Enqueue a durable report run that generates the QC universe snapshot for a given date (#1202/#1203 MR5).

Request:

{
  "snapshot_date": "2026-03-31"
}

The QC universe includes all active SNAP cases as of the snapshot date with 24 columns per the FNS-7176 specification.

Response (202): ReportRunAcceptedrun_id, generation_id, poll_url — plus a Location header pointing at the poll URL. Poll GET /v1/reporting/runs/{id} until done, then read the published snapshot from the page/CSV endpoints below.

Response (409): the target date already has a queued/running run — the body carries THAT run’s ReportRunAccepted handle to poll.

Response (503): report runs disabled (RUNS_ENABLED=false) or the run queue at capacity — retry later.

The old synchronous 201 QcSnapshotGenerated body is DELETED (pre-1.0, no shim).

Regulatory basis: 7 CFR Part 275 — QC sampling and review.

GET /v1/reporting/snap/qc-universe/{date}

One keyset page of the QC universe for a specific date (format: YYYY-MM-DD), ordered by household_id ascending (#1221; ADR-001 Amendment 1 §B2).

Query parameters:

  • limit — page size; defaults to 50, clamped to 200.

  • after_household_id — the prior page’s next_cursor.after_household_id; omit for the first page.

Since #1202/#1203 MR5 the page reads exclusively from the date’s PUBLISHED generation (staged/superseded/abandoned generations are invisible).

Response (200): SnapQcUniversePageitems (array of SnapQcUniverseEntry), next_cursor ({after_household_id} while a full page may have more, null at the end), total_in_scope (the authoritative generation-scope COUNT(*), populated only on the first page — the §B3 completeness tripwire: a page-looping extractor asserts drained rows equal it), and provenance (see "Report provenance" below).

Response (404): no published QC universe snapshot for the date. (Before MR5 an unknown date returned a 200 empty page.)

GET /v1/reporting/snap/qc-universe/{date}/csv

Export the QC universe as a CSV file (FNS-7176 column format) — rows of the date’s PUBLISHED generation; row data only, provenance rides the JSON page endpoint. A NULL abawd_household ("unverified legacy", plan D8) renders as an empty cell.

Response (200): CSV file (Content-Type: text/csv), streamed — the header first, then one chunk per keyset page, so peak service memory is O(page) regardless of snapshot size (#1221 / scale-audit H10). Inherent streaming caveat: a database error after the 200 header has gone out truncates the download mid-body rather than producing a 5xx.

Response (404): no published QC universe snapshot for the date. (Before MR5 an unknown date returned a 200 header-only CSV.)

ACF-199 Monthly TANF Case Snapshots

POST /v1/reporting/tanf/acf-199

Enqueue a durable report run that extracts one ACF-199 case snapshot per approved TANF determination for the month (#1202/#1203 MR6 — the run enriches each case with work-requirement, time-limit, work-activity, and CAPS childcare-funding data off the request path).

Request:

{
  "report_month": "2026-03-01"
}

Any day of the month is accepted; the period is canonicalized to the month start.

Response (202): ReportRunAcceptedrun_id, generation_id, poll_url — plus a Location header pointing at the poll URL. Poll GET /v1/reporting/runs/{id} until done, then read the published snapshots from the list/CSV endpoints below.

Response (409): the target month already has a queued/running run — the body carries THAT run’s ReportRunAccepted handle to poll.

Response (503): report runs disabled (RUNS_ENABLED=false) or the run queue at capacity — retry later.

The old synchronous 201 Acf199Generated body is DELETED (pre-1.0, no shim).

Regulatory basis: 45 CFR Part 265 — TANF data collection and reporting.

GET /v1/reporting/tanf/acf-199

List ACF-199 case snapshots from a PUBLISHED generation (#1202/#1203 MR6 — staged/superseded/abandoned generations are invisible).

Query parameters:

  • month (optional, YYYY-MM) — the report month. Omitted = the latest published generation. Bad format → 400.

  • limit (optional) — page size; default 50, clamped to 200.

  • after_report_month + after_case_id (optional, together) — the keyset cursor from next_cursor (#1334). Half-supplied cursor pair → 400 (a silently-restarted walk would look complete to a page-looping extractor).

Response (200): TanfAcf199SnapshotPage (#1334 — replaces the bare 100-row list) — items ordered by (report_month, case_id), next_cursor (null at the end), total_in_scope (first page only — the ADR-001 A1 §B3 completeness tripwire), and provenance (see "Report provenance" below).

Response (404): no published ACF-199 generation for the requested month — or, with month omitted, none published at all.

GET /v1/reporting/tanf/acf-199/csv

Export the resolved published generation’s ACF-199 snapshots as CSV. Same month query parameter and latest-published fallback as the list; row data only — provenance rides the JSON list endpoint. Page-walks the FULL generation (#1334 — the previous 100-row bound silently truncated larger months).

Response (200): CSV file (Content-Type: text/csv).

Response (404): no published ACF-199 generation for the month (or at all).

ACF-196 Quarterly TANF Financial Report

POST /v1/reporting/tanf/acf-196

Generate the ACF-196 quarterly stub — synchronous (local-DB aggregation over published ACF-199 generations, sub-second; plan D7/D8). Expenditure columns are zero pending real state-accounting wiring (partner-blocked).

Request: GenerateTanfQuarterlyRequestfiscal_year, fiscal_quarter (federal fiscal year; Q1 = Oct–Dec of the prior calendar year).

Response (201): Acf196Generatedfiscal_year, fiscal_quarter, categories_generated, status. Since #1202/#1203 MR6 categories_generated is rows_affected()-honest: a rerun whose rows already exist reports 0, never a fabricated total.

Response (422): the fiscal quarter’s three months lack PUBLISHED ACF-199 generations (the plan-D2 published-input guard; the refusal names the missing months) — or an invalid fiscal_quarter (expected 1-4; previously a 500).

Regulatory basis: 45 CFR Part 265 — ACF-196 TANF financial report.

GET /v1/reporting/tanf/acf-196

List ACF-196 quarterly reports, newest first (most recent 48 quarters). Bare array of TanfAcf196Report — not generation-scoped (the quarterly tables are synchronous local aggregations, not run outputs).

TANF Work Participation Rate (WPR)

POST /v1/reporting/tanf/wpr

Calculate the monthly all-family + two-parent Work Participation Rates from the month’s published ACF-199 generation — synchronous (plan D7/D8).

Request: GenerateTanfReportRequestreport_month. Any day of the month is accepted; the period is canonicalized to the month start.

Response (201): WprCalculatedreport_month (normalized), all_family_rate, two_parent_rate, all_family_meets_target, two_parent_meets_target.

Response (422): the report month has no PUBLISHED ACF-199 generation (the plan-D2 published-input guard; names the missing month).

Regulatory basis: 45 CFR 261.21 / 261.32(c) — including the two-parent 55-hour childcare-funded standard (#1169).

GET /v1/reporting/tanf/wpr

List WPR calculations, newest first (most recent 24 months). Bare array of TanfWprCalculation.

T-MSIS Eligibility Extract

POST /v1/reporting/medicaid/tmsis

Enqueue a durable report run that extracts one T-MSIS eligibility row per approved Medicaid determination for the month, with the 38-COA coverage-group mapping, FPL computation, and citizenship enrichment off the request path (#1202/#1203 MR6).

Request:

{
  "report_month": "2026-03-01"
}

Any day of the month is accepted; the period is canonicalized to the month start.

Response (202): ReportRunAcceptedrun_id, generation_id, poll_url — plus a Location header pointing at the poll URL.

Response (409): the target month already has a queued/running run — the body carries THAT run’s handle.

Response (503): report runs disabled or the run queue at capacity.

The old synchronous 201 TmsisGenerated body is DELETED (pre-1.0, no shim). Dropped/degraded rows are counted, never silent (plan D6): orphan households are excluded and counted skipped_orphan; missing person facts ship citizenship_status = "unknown" (counted citizenship_degraded) and an honest NULL FPL (counted fpl_not_computable) — all visible in provenance and the run’s detail_counters.

Regulatory basis: 42 CFR Part 431 — T-MSIS state data submission.

GET /v1/reporting/medicaid/tmsis

List T-MSIS eligibility extracts from a PUBLISHED generation.

Query parameters:

  • month (optional, YYYY-MM) — the report month. Omitted = the latest published generation. Bad format → 400.

  • limit (optional) — page size; default 50, clamped to 200.

  • after_report_month + after_enrollment_id (optional, together) — the keyset cursor from next_cursor (#1334). Half-supplied cursor pair → 400 (a silently-restarted walk would look complete to a page-looping extractor).

Response (200): MedicaidTmsisExtractPage (#1334 — replaces the bare 100-row list; the wire sort changed from person_id to (report_month, enrollment_id), the generation’s total order) — items, next_cursor, total_in_scope (first page only), provenance.

Response (404): no published T-MSIS generation for the requested month (or at all).

GET /v1/reporting/medicaid/tmsis/csv

Export the resolved published generation’s T-MSIS extract as CSV. Same month parameter and latest-published fallback; row data only — provenance rides the JSON list endpoint. Streams the FULL generation as one RFC 4180 chunk per keyset page (#1454 removed the silent 100-row truncation; #1455 added real CSV quoting — embedded commas/quotes/newlines no longer corrupt rows). The JSON list endpoint pages on the same keyset since #1334.

Response (200): CSV file (Content-Type: text/csv), streamed. A database error after the 200 header truncates the download mid-stream (inherent streaming caveat, same as the FNS-7176 export) — it cannot become a 5xx.

Response (404): no published T-MSIS generation for the month (or at all).

CMS-64 Quarterly Expenditure Report

POST /v1/reporting/medicaid/cms-64

Aggregate CMS-64 enrollment counts + member-months from the quarter’s published T-MSIS generations — synchronous (plan D7/D8). Expenditure columns are NULL pending MMIS wiring (partner-blocked).

Request: GenerateMedicaidQuarterlyRequestfiscal_year, fiscal_quarter.

Response (201): Cms64Generatedfiscal_year, fiscal_quarter, coverage_groups, status. Since #1202/#1203 MR6 coverage_groups is rows_affected()-honest: a rerun reports 0, never a fabricated total.

Response (422): the fiscal quarter’s three months lack PUBLISHED T-MSIS generations (names the missing months) — or an invalid fiscal_quarter (expected 1-4; previously a 500).

Regulatory basis: 42 CFR Part 430 — CMS-64 quarterly expense report.

GET /v1/reporting/medicaid/cms-64

List CMS-64 quarterly reports, newest first (most recent 48 quarters). Bare array of MedicaidCms64Report — not generation-scoped.

CMS-416 Annual EPSDT Report

POST /v1/reporting/medicaid/cms-416

Enqueue a durable report run that builds the annual EPSDT age-band report for a report year (#1202/#1203 MR6).

Request:

{
  "report_year": 2026
}

Response (202): ReportRunAccepted + Location header. Input pinning (plan D2): at enqueue the twelve months' published T-MSIS generation_id`s are recorded in the new generation’s `input_generation_ids; the universe drain filters generation_id = ANY(pinned), and the worker re-verifies the pins at every claim — a pin superseded/abandoned since enqueue finalizes the run error/stale_pins. CMS-416 is fully pinned and reproducible against published T-MSIS generations.

Response (409): the target year already has a queued/running run — the body carries THAT run’s handle.

Response (422): one or more months of the report year lack a PUBLISHED T-MSIS generation — the refusal names the missing months; generate + publish them first. (Also: a report year out of range.)

Response (503): report runs disabled or the run queue at capacity.

The old synchronous 201 Cms416Generated body is DELETED (pre-1.0, no shim).

Regulatory basis: 42 CFR 441.62 / SSA §1902(a)(43) — annual EPSDT participation report.

GET /v1/reporting/medicaid/cms-416

List CMS-416 age-band rows from a PUBLISHED generation.

Query parameters:

  • year (optional, integer) — the report year. Omitted = the latest published generation. Out-of-range year → 400.

  • limit (optional) — page size; default 50, clamped to 200.

  • after_report_year + after_age_group (optional, together) — the keyset cursor from next_cursor (#1334). Half-supplied cursor pair → 400 (a silently-restarted walk would look complete to a page-looping extractor).

Response (200): MedicaidCms416ReportPage (#1334) — items ordered by (report_year, age_group), next_cursor, total_in_scope (first page only), provenance. One generation holds at most the configured band set, so a single page is typical.

Response (404): no published CMS-416 generation for the requested year (or at all).

Cross-Program Overpayment Recovery Roll-Up

GET /v1/reporting/overpayments

Generate a CSV roll-up of overpayment claims + recoupment totals for one program (PAMMS 9000 / 7 CFR 273.18).

Minimum role: supervisor — via the exchanged user-context arm (#1438; user-only route: service-class tokens and, under enforcement, direct broad-audience worker bearers are 403).

Query parameters:

  • program (required) — snap / tanf / medicaid.

  • status (optional) — open / in_repayment / closed / written_off. Omitted = no filter.

Behaviour: per ADR-001, canopy-reporting cannot read program-service DBs directly. Since #1222 the roll-up drains the target program service’s keyset GET /v1/overpayments page-by-page to exhaustion — each row already carries its server-side total_recouped_cents + outstanding_cents, so there are NO per-claim ledger calls (the old serial N+1 crossed gateway timeouts at ~3K claims) — and asserts the pulled count against total_in_scope (the §B3 completeness tripwire; a truncated universe fails the run). The supervisor-dashboard summary rides the same drain. Caseworker-supplied error_type is properly CSV-quoted (commas, embedded quotes, newlines all handled).

Response (200): CSV file (Content-Type: text/csv), 12 columns:

claim_id,person_id,household_id,determination_id,claim_amount_cents,
recouped_cents,outstanding_cents,status,claim_basis,error_type,
discovered_at,closed_at

Response (400): Unknown program.

GET /v1/reporting/overpayments/summary

Aggregated overpayment summary across all programs for the supervisor-dashboard overpayment-rollup panel (Stage 5 MR2 #496 FU-10).

Minimum role: supervisor (or service-class token).

Behaviour: per ADR-001, canopy-reporting cannot read program-service DBs directly. It composes via HTTP, fetching claims from each program service through the existing list_overpayment_claims client method + ledger view, then aggregates totals (no per-claim row data).

Response (200): OverpaymentsSummarytotal_open_claims, total_closed_claims, total_outstanding_cents, total_recouped_cents, plus by_program array of ProgramOverpaymentTotals (per-program open_claims, closed_claims, outstanding_cents, recouped_cents).

Report Runs (#1202/#1203)

Durable report-run status surface (plan report-run-generations D7). MR4 shipped the READ surface + the run substrate; MR5 (SNAP) flipped the two SNAP generate POSTs, and MR6 (TANF/Medicaid) the remaining three — all five federal generate POSTs now enqueue and answer 202 ReportRunAccepted{run_id, generation_id, poll_url} (ACF-196 / CMS-64 / WPR stay synchronous local aggregations). Operational guide: the report-runs runbook.

Access (both endpoints): supervisor-or-above OR any service-class caller — an explicit OR. Runs are org-visible (a deliberate deviation from the #1205 chain-job requester-scoping: a hidden colleague run would make the 409 handles un-pollable).

GET /v1/reporting/runs/{id}

Poll one run by the id from the 202/409 enqueue handle.

Response (200): ReportRunStatusrun_id, generation_id, report_kind (fns_388 | qc_7176 | acf_199 | tmsis | cms_416), period_date, state (queued | running | done | error), attempts, universe_total, processed_count, skipped_orphan_count, degraded_count, detail_counters (named counts from the GENERATION row — never decoded from run progress, so a malformed progress blob can never 500 a poll), error_code (upstream_unavailable | universe_drift | contract_violation | stale_pins | crashed | immutable_target), abandon_reason, requested_at, finished_at, and result_url (set once done, pointing at the kind’s read endpoint above).

Queued/running responses carry a Retry-After header (one worker tick, whole seconds).

Response (404): unknown id — or an already-reaped terminal run (indistinguishable by design; terminal run rows reap after RUN_REAP_DAYS, provenance survives on the permanent generation row).

GET /v1/reporting/runs

List runs, newest-requested first.

Query parameters: kind (optional filter; unknown value → 422), period (optional canonical period date), limit (clamped to the house bounds: default 50, max 200).

Response (200): Vec<ReportRunStatus> ordered by requested_at descending.

Report provenance

Since #1202/#1203 MR5 (SNAP) and MR6 (TANF/Medicaid) every generation-scoped read body — FNS-388 get/list, the QC universe page, the ACF-199 / T-MSIS / CMS-416 lists — carries a typed provenance object from the resolved published report_generations row: RunProvenance{generation_id, run_id, state, universe_total, processed_count, skipped_orphan_count, degraded_count, extracted_at, as_of}. run_id is null for the pre-pipeline legacy backfill generations; state is always published on a read (readers never serve any other state); extracted_at is the honesty stamp for the as-of-extraction enrichment legs (see Known limitations); as_of is the persons valid-time pin the run read under. Provenance survives run reaping — it lives on the permanent generation row. The CSVs (QC, ACF-199, T-MSIS) are row-data-only; provenance rides each kind’s JSON page/list endpoint.

Known limitations

As-of-extraction enrichment (plan report-run-generations D2). Run universes are reproducible: phase 1 of every run materializes the universe into report_run_universe and processing iterates that snapshot, and the persons enrichment is pinned (as_of = the period end for monthly kinds, the snapshot date for QC-7176 — surfaced as provenance.as_of). The applications, enrollment-issuance, SNAP-ABAWD, TANF (work-requirement / time-limit / work-activity), and CAPS (childcare-authorization) enrichment legs have no valid-time corpus, however: they are read as-of-extraction, so re-running the same period after upstream edits can legitimately produce different enrichment values. The generation’s extracted_at (surfaced in provenance) records the read time. Valid-time corpora for those sources are filed follow-ups (#1331 covers the ABAWD corpus). The exception is CMS-416, which has no as-of-extraction leg at all: its inputs are pinned T-MSIS generation ids recorded at enqueue, so a CMS-416 report is fully reproducible against its published T-MSIS generations.

Federal-field dispositions (plan D8, resolved by #1333). Every formerly-silent approximation now has an explicit per-field disposition — sourced, NULL-as-unknown, or documented:

  • Sourced — T-MSIS eligibility_start_date/eligibility_end_date come from the determination’s own coverage window (the start falls back to the report month only when the wire carried no effective date); restricted_benefits_indicator derives from the determination’s benefit_type (family-planning-only and QMB-supplement coverage IS restricted scope; a missing benefit_type maps to unrestricted — restricted scope is asserted from positive evidence only). CMS-416 total_member_months and CMS-64 member_months are sourced from the run’s PINNED T-MSIS generations (#1585): one member-month = one DISTINCT (person, report_month) in scope — extract rows key on the enrollment (one per approved determination), so a same-month re-determination never double-counts a month — computed locally, no new upstream leg. A CMS-416 row still carries NULL when the figure is honestly unavailable (a run resumed from a pre-#1585 checkpoint, or months missing for a banded person) — never a fabricated value.

  • NULL = unknown — ACF-199 months_other_states (no interstate TANF interchange source). A NULL is never a fabricated figure.

  • Documented — CMS-416 eligible_for_screening equals total_enrolled_children as this report’s operating identity: banded (under-21, non-CHIP) enrollees hold the 42 CFR 441.56 EPSDT entitlement, so the count is definitional here rather than an unmarked approximation; T-MSIS managed_care_enrolled stays false — canopy has no MCO/MMIS source (partner-blocked); ACF-199 closure_reason NULL means UNKNOWN (no case-closure leg exists), never "the case is open".

Error Codes

Code Meaning

400

Invalid month/date/year format (or out-of-range year on the CMS-416 list), or unknown program value on the overpayments roll-up

401

Missing or invalid JWT

403

User-only surfaces (#1438): supervisor-or-above via the exchanged user-context arm — service_class_on_user_only for service bearers, aud_not_exact for direct broad-audience worker bearers under enforcement, or the role bar for exchanged non-supervisors. The dual run/summary endpoints also admit service-class callers

404

Report, snapshot, or run not found — for the generation-scoped reads: no PUBLISHED generation for the period, or none at all under the latest-published fallback (a reaped run is an indistinguishable 404)

409

The target period already has a queued/running run — the body carries that run’s ReportRunAccepted handle (all five generate POSTs)

422

Unknown kind value on the run list; a published-input guard refusal naming the missing months (CMS-416 enqueue pinning; ACF-196 / CMS-64 / WPR inputs); an invalid fiscal quarter

500

Upstream service failure during the synchronous overpayments roll-up / summary assembly — the only handlers that still call upstream services on the request path; the body is redacted ("An unexpected error occurred"). The five generate POSTs never assemble synchronously: an upstream failure there surfaces as a run outcome on the poll endpoint (error_code/abandon_reason), not as an HTTP error. (No endpoint returns 502; the old synchronous TANF/Medicaid 502 arm is gone with the MR6 cutover.)

503

Report runs disabled (RUNS_ENABLED=false) or the run queue at capacity — all five generate POSTs; also a DB pool-acquire timeout anywhere (#1296)

Upstream Service Clients

canopy-reporting queries these services via HTTP (configured by environment variables):

Service Env Var Data Retrieved

canopy-persons

CANOPY_REPORTING__PERSONS_URL

Household demographics

canopy-applications

CANOPY_REPORTING__APPLICATIONS_URL

Application dates and status

canopy-snap

CANOPY_REPORTING__SNAP_URL

ABAWD tracking data

canopy-enrollment

CANOPY_REPORTING__ENROLLMENT_URL

Benefit issuance records

canopy-renewals

CANOPY_REPORTING__RENEWALS_URL

Certification periods

canopy-tanf

CANOPY_REPORTING__TANF_URL

TANF determinations, work requirements, time limits, work-activity summaries (ACF-199)

canopy-medicaid

CANOPY_REPORTING__MEDICAID_URL

Medicaid determinations with the 38-COA coverage-group mapping (T-MSIS)

canopy-caps

CANOPY_REPORTING__CAPS_URL

Active childcare authorizations — the WPR two-parent 55-hour standard discriminator (#1169)

Edit this page · default