canopy-snap API Reference
On this page
Overview
Cross-link: canopy-snap Data Model (#419)
SNAP program service. Computes eligibility determinations (income tests, deductions, allotment), manages ABAWD work requirement tracking, categorical eligibility (BBCE), student status screening, IEVS verification results, and exposes jurisdiction parameters.
All eligibility logic runs through the rules engine (ADR-003) — no federal regulation values are hardcoded.
- Base URL
- Authentication
-
Bearer token (Keycloak RS256 JWT)
- Minimum role
-
Varies per endpoint (see below)
- Swagger UI
- Database
-
canopy_snap(isolated per ADR-001; contains IEVS data per ADR-004)
Receiver contract (OIDC S-snap, #1431 / ADR-043 §C)
canopy-snap is the seventh service on the ADR-043 receiver contract
(canopy_auth::ReceiverContract) — see the
tanf API page for the full bearer-shape and guard-family description —
and a TERMINAL exchange target (single-exact aud=canopy-snap; the
hop-2 pair shape is eligibility-only).
The snap specifics:
-
require_service_or_exchangedonPOST /v1/determine— the orchestrator’s service token, or the hop-2 exchanged bearer it re-exchanges from the S-eligibility pair (devstackEXCHANGE_TARGETSincludes canopy-snap). A direct worker bearer stays 403 (the #439 posture)./v1/determine/dry-runstays service-only (a #1213 self-call surface). -
require_service_or_exchangedalso onPOST /v1/determinations/{id}/overpayment-recompute— the BFF sends the worker’s exchanged bearer andrequested_byrecords the worker’s own subject (service class / CLI keeps working). -
require_user_onlyon the determination redact (data_steward) and the QC export (admin/quality_control). Service bearers are always 403; underCANOPY_SNAP__ENFORCE_USER_ONLY_ROUTES=true(devstack: on) only an exchanged per-target token carrying the role passes. -
The
as_of/triggerorchestrator-only pins accept both orchestrator shapes: the exactcanopy-eligibilityidentity orazp=canopy-eligibility-exchanger(only eligibility’s exchanger can mint that shape; the UTC-vs-legal-timezone date seam behind the pin is #1561). -
Attribution via
EffectiveUserat the redaction event, export audit, and recomputecaller_uuidsites. Body-suppliedresolved_by_sub/discovered_byare unchanged (#874’s scope). -
Azp allowlist:
canopy-web-exchanger,canopy-eligibility-exchanger(snap is anEXCHANGE_TARGETSprogram).
Determination
POST /v1/determine
Run a SNAP eligibility determination. Called by canopy-eligibility orchestrator.
Minimum role: eligibility_specialist
Request:
{
"application_id": "uuid",
"household_id": "uuid",
"household_size": 3,
"members": [
{
"person_id": "uuid",
"ssn": "123-45-6789",
"date_of_birth": "1990-05-15",
"income": [ { "type": "employment", "monthly_amount": 1500.00 } ],
"assets": [ { "type": "bank_account", "value": 2000.00 } ],
"expenses": [ { "type": "rent", "monthly_amount": 800.00 } ]
}
]
}
The handler:
-
Evaluates gross income test (130% FPL via rules engine)
-
Computes deductions (standard, earned income, dependent care, medical, shelter/SUA). For self-employment income, pre-processes via the
georgia-snap-self-employment-deductionJDM ruleset: net SE = gross SE − max(actual_business_expenses, gross_SE × 40%) (PAMMS 3425 / 7 CFR 273.11(a)(2)). Applicants with zero reported actual business expenses (common for service workers) get the 40% standard rather than being penalized. -
Evaluates net income test (100% FPL via rules engine)
-
Calculates benefit allotment (max allotment - 30% net income)
-
Assembles + persists the determination input snapshot (ADR-028): the proven facts with provenance + a correction-stable fact id (re-parsed from the orchestrator-forwarded persons facts), the resolved policy params, the exact evaluated input, and the ruleset corpus-hash — joining the snap-local IEVS reconstruction for any IEVS-authored income fact. Its SHA-256 (RFC 8785 canonical) becomes the
snapshot_hashsigned into the determination; the snapshot is stored immutably indetermination_snapshotsin the same transaction. -
Records supersession (ADR-028 §57): when the request carries
previous_determination_id(an optional context field — a recert/adjustment re-determination supplied by the caller), it is validated (the antecedent must exist, belong to the same household, and not already be superseded — else 422/409) and bound into the signed envelope before signing. Production callers do not set it yet (the recert→determine trigger is a follow-up); absent it, the determination supersedes nothing. -
Signs the determination with ECDSA P-256 (ADR-002) — the signature now covers
snapshot_hash+previous_determination_id -
Triggers IEVS verification asynchronously
as_of-faithful evaluation (#1467, ADR-028 Amendment 6). The optional as_of context field is the evaluation date: it selects the effective-dated parameter set in force (snap-{allotments,deductions,income-limits}-* grouped by _effective_date, validity [start, next-Oct-1)), anchors the benefit-period dates (effective_date = as_of; expiration/renewal via the configured certification/renewal months), the snapshot as_of, and a denial’s snap.case_closed.closure_date. Only determined_at stays wall-clock. Absent, it falls back to today as a legal date in the jurisdiction’s timezone. Guards, in order, all before any evaluation or write:
-
jurisdictiondiffering from the service’s configured jurisdiction → 422. -
A non-fallback
as_offrom any identity other thancanopy-eligibility→ 403 (time-travel is orchestrator-only). -
No parameter set in force at
as_of(gap, or October 1 arrived without the new files) → 422, unless the accountableCANOPY_SNAP__ALLOW_EXPIRED_PARAM_SET=trueoverride serves the newest out-of-window set with a start on or beforeas_of(error-logged per use; anas_ofpredating every set stays 422 regardless). -
expected_policy_target(optional) differing from the resolved{corpus_hash, params_digest, effective_period}→ 409policy_target_mismatch— the pre-write pin a bulk dispatcher uses so a policy skew can never be discovered after effects shipped.
Response (200): SignableDetermination — the universal signed-determination envelope (ADR-002) with status, benefit amount, basis, JWS signature, a program-specific program_extension payload, the snapshot_hash binding the input snapshot (ADR-028), and the optional previous_determination_id supersession link (ADR-028 §57). When emit_policy_attestation is enabled (devstack: on; production: after the fleet carries the tolerant envelope — see the scaling runbook), the envelope additionally binds policy_target (the composite policy identity) and evaluated_as_of (the exact evaluation date, present for denials too). The orchestrator verifies the JWS signature against this same shape and receives only outcome + hash (never the snapshot cleartext).
POST /v1/determine/dry-run
Non-persisting dry-run (T2-7, #680; ADR-027 §6) against exactly ONE policy source — the frozen bundle of a determination-of-record (the T2-7 materiality replay) or, since #1472, a caller-named target policy (the COLA preview). Called by the canopy-eligibility orchestrator; not a worker-facing endpoint.
Minimum role: service caller (ADR-019 — same gate as POST /v1/determine; no row is written, but the dry-run replays sealed-determination policy).
Request: DryRunDetermineRequest
{
"context": { /* the same ApplicationContext shape as POST /v1/determine, with the current facts */ },
// Baseline replay (T2-7): both fields together, no target_policy —
"policy_bundle": { /* the SnapPolicyBundle read from the baseline determination's snapshot.policy_params */ },
"corpus_hash": "<the baseline determination's ruleset corpus version to replay>",
// — OR target policy (#1472): alone —
"target_policy": { "corpus_hash": "<hex64>", "params_digest": "<hex64>" }
}
Any other combination (both sources, neither, or a bundle without its corpus pin) is a 400.
Baseline replay runs the shared evaluate_verdict pipeline — identical to POST /v1/determine — reading policy only from the supplied policy_bundle (the complete frozen set: the 15 thresholds + pay-period factors + SE-deduction settings), never the live parameter table.
Target policy (#1472) resolves params_digest against the loaded parameter sets (#1467) — content-addressed and deliberately date-blind, so a staged next-window set (the October COLA files) is previewable before its window opens — and pins every rules call to target_policy.corpus_hash. It requires an explicit context.as_of (400 without one: a policy preview has no implicit evaluation date) and the configured jurisdiction (422 on mismatch); a digest matching no loaded set is a fail-closed 422 (stage the target snap-cola files and restart). The live path’s expired-set guard does not apply here — validity is echoed, not gated, because nothing is written.
Both arms are write-free end to end: no DEK, nothing sealed, nothing signed, no application / determination / snapshot / events persisted. The corpus pin + audit=false thread through all three rules calls (the self-employment pre-pass, the per-member alien pre-check, and the main eligibility ruleset — Decision C/D), so canopy-rules writes no audit rows either.
Response (200): DryRunOutcome — { status, benefit_amount, benefit_unit, corpus_hash_used, resolved_target? }. Unsigned: a dry-run is not a determination of record (ADR-002 signing attaches only to persisted determinations). corpus_hash_used echoes the pinned corpus. In target mode resolved_target carries the full PolicyTarget — the requested pins plus the matched set’s intrinsic effective_period (snap’s resolution, never a caller assertion); it is absent on baseline replay, keeping that wire byte-identical. Returns 400 on an invalid body (e.g. household_size = 0), 403 for a non-service caller, and 422 when the pinned corpus is unknown to canopy-rules (CorpusUnavailable) or the target digest matches no loaded set — the caller degrades to manual review rather than failing.
GET /v1/determinations/{id}
Get a determination by ID.
Minimum role: caseworker
Response (200): SnapDeterminationRead — the SnapDetermination fields (flattened, including snapshot_hash + the previous_determination_id it supersedes) including the required snapshot_hash (#911 retired the ADR-028 §58 snapshot_status marker) and the derived supersession edge (ADR-028 §57): superseded_by_id (the determination that supersedes this one, if any) + superseded_as_of (the world-date from which it stopped being operative — the superseder’s effective date, falling back to the superseder’s snapshot evaluation date for an adverse re-determination with no effective date). Both are absent when this is the operative (latest) determination in its chain. Returns 404 if no determination exists for the supplied ID.
GET /v1/determinations
List determinations newest-first (paginated via page / per_page), optionally scoped to one household via household_id (#1576 — the server-side filter tanf/medicaid gained in #1244 and caps/wic always had; the scoped read rides a composite (household_id, created_at DESC, id DESC) index, and the id tiebreak makes equal-timestamp rows page deterministically).
Minimum role: caseworker
Response (200): array of SnapDeterminationRead (each with its derived supersession edge — resolved in the same read, so list rows are accurate, never a misleading absent value).
GET /v1/determinations/{id}/snapshot
Return a determination’s frozen ADR-028 §57 input snapshot — the cross-service read for materiality/overpayment/QC consumers.
Minimum role: service caller OR admin / quality_control. The full snapshot carries proven facts, provenance, income, household composition, and DOB/disability, so it is restricted to those callers — NOT general caseworkers.
Response (200): the typed DeterminationSnapshot JSONB blob (proven facts with provenance, resolved policy params, exact evaluated input, ruleset corpus-hash). Returns 404 if no determination exists for the ID, or if it is a legacy determination with no input snapshot (a distinct "no input snapshot" message, ADR-028 §58). A determination whose signed snapshot_hash is present but whose stored snapshot blob is missing is 500 (data corruption, alerted) — never silently served. FTI programs' (tanf/medicaid) hearing-scoped in-boundary snapshot read is a separate follow-up; this endpoint is SNAP (non-FTI) only.
POST /v1/determinations/{id}/redact
Crypto-shred a determination’s frozen input snapshot (T2-6 #687, ADR-036). The per-determination DEK in redaction_keys is tombstoned (its wrapped_dek overwritten with a zero sentinel + shredded_at stamped), so every sealed leaf becomes permanently unrecoverable, while the snapshot ciphertext and the signed snapshot_hash are left untouched — the snapshot still re-hashes to the signed value and the determination’s JWS stays verifiable (hash-over-ciphertext, ADR-036 Decision B). Only the plaintext PII is destroyed.
Minimum role: data_steward only. This is a dedicated, privileged, irreversible role for redaction/expungement — admins do NOT auto-hold it (separation of duties; admins grant/revoke the role but do not wield redaction authority themselves, mirroring fti_auditor).
Request:
{
"reason": "..."
}
reason is mandatory; a blank reason is rejected with HTTP 400.
The shred and a plaintext-free determination.redacted audit event (carrying the steward’s sub + the reason) commit in one transaction (ADR-018); canopy-security audits it via the existing wildcard subscriber.
Response (200): { "determination_id": "…", "redacted_at": "…" }. Returns 400 on a blank reason, 403 if the caller lacks the data_steward role, and 404 for an unknown determination. Idempotent: re-redacting an already-shredded determination tombstones 0 rows and still returns 200.
The path uses the sub-resource form …/{id}/redact (mirroring …/{id}/resolve), not the AIP-136 custom-method …/{id}:redact — axum/matchit 0.8 allows only one parameter per path segment, so a {id}:redact segment is unroutable.
GET /v1/determinations/{id}/hearing-view
Return a determination’s hearing-scoped, FTI-safe projection (T2-8 #681, ADR-028 §70) — the in-boundary read canopy-appeals consumes to display a frozen determination at a hearing, without ever pulling a sealed leaf or restricted value out of the owning program service.
Minimum role: service caller. Unlike /snapshot (which returns the sealed blob to service/admin/QC), this is a distinct, unsealed, non-restricted projection: canopy-snap unseals the snapshot service-locally (it owns the DEK) and projects to a DTO that carries only the verdict (status, benefit, dates), the pinned corpus_hash + policy_params_digest, the household size, and a facts_summary of input-fact identities (kind / optional person_id / optional fact_id / label) — never a fact value. FTI-safety is by construction of the projection, so an FTI program (tanf/medicaid) returns an FTI-redacted shape unchanged.
Response (200): HearingDeterminationView. Returns 404 for an unknown determination or a legacy determination with no input snapshot, 422 for a non-SNAP determination, 403 for a non-service caller.
POST /v1/determinations/{id}/overpayment-recompute
Replay a determination’s frozen snapshot against the corrected household facts to size an overpayment, file the #382 claim, and emit snap.overpayment_claimed → OverpaymentNotice (T2-8 #681, ADR-028 §70). Worker-actioned (a retroactive fact correction whose valid_from <= snapshot.as_of revealed the past determination was wrong); the recompute runs entirely in canopy-snap (which owns the snapshot + the per-determination DEK), so FTI never crosses to canopy-appeals/canopy-reporting (ADR-004).
Minimum role: service caller (the worker portal / CLI mediates the call; requested_by is derived from the authenticated actor, not a body field).
Request: OverpaymentRecomputeRequest
{
"correction_as_of": "2026-03-01",
"claim_basis": "agency_error"
}
correction_as_of bounds the clawback window (it must be <= the determination’s evaluation date — a forward-effective change is a redetermination, not an overpayment, and is rejected 422); claim_basis ∈ {agency_error, inadvertent_household_error}.
Flow: idempotency lookup (unique (determination, correction_as_of)) → per-household pg_advisory_xact_lock → resolve the baseline snapshot → provisional-derived guard (any provisional node → excluded) → overlap guard (a non-closed claim / intersecting prior recompute → manual review) → derive the recipient (snapshot head-of-household) → unseal the frozen derived inputs in-boundary + re-fetch the corrected income/asset/expense leaves from canopy-persons as-of snapshot.as_of → replay via the write-free dry_run_determine (frozen bundle + pinned corpus) → size the per-month delta (Σ max(0, paid − correct), excluding retained issuances) → on a positive overpayment, create_claim + an overpayment_recomputes audit row + the outbox event, in one transaction.
Response (200): OverpaymentRecomputeResult — a typed outcome (claim_created | no_overpayment | underpayment_found | below_threshold | provisional_excluded | overlapping_claim | no_baseline_snapshot | corpus_unavailable), the sized overpayment_cents + affected_months, the baseline/correct verdict refs, and claim_id when a claim was created. Degraded outcomes (no snapshot / unavailable corpus / shredded DEK) return 200 with an outcome_message (manual review), never a 500 or a wrong claim. Returns 404 for an unknown determination, 422 for a forward-effective correction_as_of.
SNAP Export
GET /v1/export/determinations
Admin-or-QC bulk export of SNAP determinations within a time window, rendered as CSV.
Minimum role: admin or quality_control
Query parameters: from, to (RFC 3339 date-times, optional), format (optional), limit (optional).
Response (200): determinations within the requested window as text/csv. Returns 400 for an invalid window or limit, and 403 if the caller lacks the admin / quality_control role.
ABAWD Work Requirements
POST /v1/abawd/activity
Record monthly work activity for an ABAWD-tracked individual.
Minimum role: caseworker
Request:
{
"person_id": "uuid",
"tracking_id": "uuid",
"benefit_month": "2026-04",
"hours_worked": 40,
"hours_job_search": 20,
"hours_training": 20
}
Qualifying: total hours >= jurisdiction.toml [snap.abawd.qualifying_hours_per_month] (default: 80). After 3 non-qualifying months in a 36-month window, a time_limit_reached event is published.
GET /v1/abawd/tracking
List ABAWD tracking records.
Minimum role: caseworker
Query parameters: person_id
POST /v1/abawd/tracking:batchGet
Get the ABAWD flag for a set of households in one round-trip (#1203, D5
row 7) — one household_id = ANY($1) set query replacing the QC extract’s
per-case tracking GET. Capped at 500 IDs per request (422 on
overflow); duplicates collapse to one entry; the response follows
first-occurrence request order. Service callers only (§B4 bulk-read
posture) — 403 for worker JWTs, even though the interactive
per-household GET above stays caseworker-reachable.
GET-OR-FALSE, exact-set: every requested UNIQUE id gets exactly one
entry — is_abawd_household: false = honestly no qualifying tracking on
file (no rows, or none in a counting status), present so consumers can
assert exact id-set equality. The response is THE BOOL the QC fold
computes today — current_status ∈ {tracking, exhausted} OR-folded over
the household’s tracking records (the consumer’s is_abawd_household
predicate, replicated server-side; no active filter, matching the GET
the fold read) — never the record vector, so a 500-household response is
bounded by construction.
as_of rides the request per the frozen #1203 wire shape but scopes
nothing today: the predicate reads the CURRENT tracking status. It is
reserved for the valid-time ABAWD corpus follow-up (#1331), which makes a
resumed historical extract reproducible without a wire change.
Request: BatchAbawdTrackingRequest
{ "household_ids": ["uuid", "uuid"], "as_of": "2026-06-30" }
Response (200): Vec<HouseholdAbawdEntry> —
[{ household_id, is_abawd_household }].
Categorical Eligibility
IEVS Verification
GET /v1/verification/discrepancies
List income discrepancies for an application or household.
Minimum role: eligibility_specialist
Query parameters: exactly one of application_id (the SHARED eligibility application id — the namespace IEVS rows key on, NOT the snap-local snap_applications.id) or household_id (#962, via the match-results join — what the worker income tab uses). Neither or both → 422.
PUT /v1/verification/discrepancies/{id}/resolve
Resolve an income discrepancy.
Minimum role: eligibility_specialist
Request:
{
"resolution_status": "accepted_verified",
"resolution_notes": "Confirmed via IEVS wage match.",
"resolved_by_sub": "kc-worker-1"
}
Resolution statuses: confirmed_additional_income, corrected_ievs_error,
resolved_household_explanation, aged_out, and (T1-9 #677) the worker
accept/reject outcomes accepted_verified / rejected. resolved_by_sub (T1-9
#677) records the resolving worker’s keycloak sub (the resolver is a caseworker,
not a PersonId; the ADR-019 on-behalf-of bridge). The resolution and its
attributed ievs.discrepancy_resolved event commit atomically (ADR-018); the
event carries IDs + status + actor only, never an income figure (ADR-004
§2025(e)). The verified income write-back into canopy-persons is performed by
canopy-web (the cross-service writer), not canopy-snap.
Parameters
GET /v1/params
Get SNAP parameters for a given household size (loaded from rules engine, not hardcoded).
Minimum role: dual-or-portal (#1441: service, caseworker-or-above worker, or the scoped portal credential on portal:snap-params:read — the portal’s ONE snap surface)
Query parameters: household_size (required), has_elderly_disabled (optional, default: false)
Response (200):
{
"household_size": 3,
"gross_income_limit": 2311,
"net_income_limit": 1778,
"max_allotment": 740,
"standard_deduction": 198,
"has_elderly_disabled": false
}
Values come from the effective-dated rulesets/federal/snap-*.json set in force today (legal date in the jurisdiction timezone, #1467) — a request when NO set is in force (e.g. October 1 arrived before the new FY files were staged) answers 422 unless the CANOPY_SNAP__ALLOW_EXPIRED_PARAM_SET override is on.
GET /v1/params/provenance
The parameter-set identity in force at an as_of (#1467, ADR-028 Amendment 6): the content digest of the complete policy-parameter input set, its intrinsic validity window, and the jurisdiction table revision. A bulk dispatcher (#1213) composes this with canopy-rules GET /v1/corpus into the expected_policy_target pre-write pin.
Minimum role: service-caller
Query parameters: as_of (optional date; default = today as a legal date in the jurisdiction timezone)
Response (200):
{
"as_of": "2026-06-02",
"params_digest": "<hex sha-256>",
"effective_period": { "start": "2025-10-01", "end_exclusive": "2026-10-01" },
"policy_params_version": "1.0.0"
}
Response (422): no parameter set in force at as_of.
Overpayment Recovery (PAMMS 9000 / 7 CFR 273.18)
Cross-program shape — same surface on canopy-tanf (/v1/overpayments) and canopy-medicaid (/v1/overpayments), with per-service data isolation per ADR-001. Types shared from the canopy-overpayments crate. Ledger is the system of record; outstanding balance is derived (not stored).
POST /v1/overpayments
File an overpayment claim.
Minimum role: caseworker (or service-class token).
Request:
{
"person_id": "...",
"household_id": "...",
"determination_id": "...",
"claim_amount_cents": 10000,
"claim_basis": "inadvertent_household_error",
"error_type": "income-misreported",
"discovered_at": "2026-05-01",
"discovered_by": "..."
}
claim_basis ∈ agency_error / inadvertent_household_error / ipv. claim_amount_cents must be > 0 (rejected with HTTP 400 otherwise).
Response 201: the persisted OverpaymentClaim row (status begins at open).
GET /v1/overpayments[?status=…&limit=…&after_created_at=…&after_id=…]
One keyset page of claims WITH server-side ledger totals (#1222, ADR-001 Amendment 1 §B2/§B3): { items: [{ claim, total_recouped_cents, outstanding_cents }], next_cursor, total_in_scope }, newest-first over (created_at, id) (served by overpayment_claims_keyset). limit defaults to 200 (cap 500); the cursor pair travels together (half-specified ⇒ 400); total_in_scope — the completeness tripwire for the PAMMS-9000 / 7 CFR 273.18 roll-up — rides the first page only. Used by `canopy-reporting’s roll-up CSV + supervisor summary, which loop to exhaustion and never make per-claim ledger calls; per ADR-001 reporting cannot read the DB directly.
status ∈ open / in_repayment / closed / written_off.
POST /v1/overpayments/{id}/repayment-plans
Attach a repayment plan. Rejected with HTTP 409 if the claim is closed or written_off.
Request:
{
"monthly_amount_cents": 2500,
"starts_on": "2026-06-01"
}
POST /v1/overpayments/{id}/recoupments
Append a recoupment ledger entry. Inside the same TX, if the new entry drives outstanding to ≤ 0 the claim status flips to closed + closed_at is stamped. The first non-zero recoupment on an open claim flips status to in_repayment (so dashboards can distinguish untouched claims from in-progress collections).
Rejected with HTTP 409 if the claim is already closed / written_off. Rejected with HTTP 400 if amount_cents == 0 (a corrective no-op should use manual_adjustment with a non-zero compensating amount).
Request:
{
"amount_cents": 2500,
"method": "allotment_reduction",
"repayment_plan_id": "...",
"notes": "month 1"
}
method ∈ allotment_reduction / cash_payment / tax_offset / write_off / manual_adjustment. Negative amount_cents is allowed (compensating reversal of a previously-recorded recoupment).
Error Codes
| Code | Meaning |
|---|---|
400 |
Invalid input (missing members, invalid income type, non-positive claim/recoupment amount, invalid export window or limit, etc.) |
401 |
Missing or invalid JWT |
403 |
Insufficient role for the requested endpoint (e.g. export requires admin / quality_control; redact requires data_steward) |
404 |
Determination, overpayment claim, TSNAP certification, or discrepancy not found |
409 |
Overpayment claim is in a terminal state ( |
Events Published
-
determination.completed.snap(IDs and status only — no PII or income data per ADR-004) -
abawd.warning(month 1 and 2 non-qualifying) -
abawd.time_limit_reached(month 3) -
ievs.discrepancy_resolved(T1-9 #677) — a worker accepted/rejected an IEVS income discrepancy. Carries the resolving worker as a nestedauthorobject ({author_type, sub}, the shape canopy-security’s event parser reads) plus the discrepancy/application/person ids,resolution_status, andincome_type— no income figure (ADR-004 §2025(e)) -
determination.redacted(T2-6 #687) — a data steward crypto-shredded a determination’s snapshot; carries the actorsub+ reason, no plaintext -
snap.overpayment_claimed(T2-8 #681; #994 appeal path) — staged in the SAME tx as the claim it announces (recompute, IPV-IHE, and appeal paths); #1105 addedappeal_id/assessment_idso canopy-appeals' acknowledgment consumer can flip its assessmentcomputed → applied, and the appeal subscriber re-emits it (select-first onassessment_id) when a re-emitted assessed event finds the claim already open
Events Consumed
-
appeal.overpayment_assessed(filtersprogram == "snap", queuecanopy-snap.overpayments) — auto-opens theOverpaymentClaimwith the #1104 provenance quad; #1105: an event whoseassessment_idalready has a claim RE-ACKNOWLEDGES withsnap.overpayment_claimedinstead of trippingoverpayment_claims_assessment_onceinto the DLQ -
appeal.overpayment_assessment_voided(#1105, same queue) — voids the claim stamped with the event’sassessment_id(idempotent; no-claim is a no-op)
Bulk markers on POST /v1/determine (#1213 D-6)
ApplicationContext.trigger (the ADR-002 A1 D9 cause class) and expected_policy_target are the BULK
MARKERS: asserting a trigger requires the exact canopy-eligibility identity (403), and a bulk-marked
context on a replica with emit_policy_attestation=false is refused 422 attestation_disabled BEFORE
any evaluation or write (B8 — the writer fails closed). The signed envelope binds trigger pre-sign;
GET /v1/params/provenance now reports attestation_enabled (absent ⇒ false, so the #1213 enact
preflight fails closed against a pre-#1213 fleet). Machine codes on this surface:
supersession_conflict, policy_target_mismatch, attestation_disabled.