canopy-wic API Reference

On this page

Overview

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

WIC program service (7 CFR Part 246). Computes categorical eligibility across five participant categories (pregnant, postpartum, breastfeeding, infant, child), income eligibility against 185% FPL with adjunctive auto-qualify via SNAP / Medicaid / TANF enrollment, and gates final certification on a recorded clinical nutritional risk assessment. Issues food-package families per 7 CFR 246.10(e)(1)-(7) (I/II/IV/V/VI/VII — Package III and (e) subpackage granularity are catalogued gaps, #1525) with category-driven certification periods per 7 CFR 246.7(g).

All eligibility logic runs through the rules engine (ADR-003) — wic-eligibility.json ruleset combines a categorical decision table with an income-threshold expression.

Base URL

http://localhost:8017/v1

Authentication

Bearer token (Keycloak RS256 JWT)

Minimum role

Varies per endpoint

Swagger UI

http://localhost:8017/swagger-ui

Database

canopy_wic (isolated per ADR-001; no FTI scope — state-administered)

Receiver contract (OIDC S-wic, #1433 / ADR-043 §C)

canopy-wic is the ninth 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-wic). The wic specifics:

  • require_service_or_exchanged on POST /v1/determine (devstack EXCHANGE_TARGETS includes canopy-wic) and on the appointment create (POST /v1/wic/households/{household_id}/appointments) — both wic BFF senders (appointment scheduling, nutritional-risk write) send the worker’s exchanged bearer. Direct worker bearers stay 403.

  • require_user_only(["data_steward"]) on the determination redact (the caps twin); EffectiveUser on the redaction event actor. Devstack enforces the broad-audience kill.

  • Azp allowlist: canopy-web-exchanger,canopy-eligibility-exchanger. The upcoming-appointments dashboard feed stays service-only.

Determination

POST /v1/determine

Run a WIC eligibility determination. Called by canopy-eligibility orchestrator.

Minimum role: service-class token (orchestrator).

Request: WicApplicationContext — a household economic unit (gross_monthly_income + household_size, the only shared input per 7 CFR 246.7) carrying a participants[] list. Each participant carries its own person_id, participant category (pregnant / postpartum / breastfeeding / infant / child), breastfeeding/infant food-package facts, adjunctive enrollment flag (SNAP/Medicaid/TANF), and nutritional-risk flag. WIC is multi-subject (ADR-035): each participant is certified independently.

The handler, per participant:

  1. Confirms participant category eligibility via the categorical decision table

  2. Adjunctive shortcut: if enrolled in SNAP / Medicaid / TANF, income test bypassed

  3. Otherwise evaluates the shared economic-unit income against 185% FPL from fpl-2026.json + jurisdiction.toml

  4. Gates final certification on a recorded nutritional risk assessment (verified from wic_nutritional_risk_assessments, not the wire flag — don’t-trust-caller)

  5. Assigns the food-package family per 7 CFR 246.10(e)(1)-(7) based on the participant’s category, feeding intensity, and (for infants) age

  6. Sets the certification period per 7 CFR 246.7(g) (varies by category — infants up to first birthday, pregnant through 6 weeks postpartum, etc.; the month-count model is a catalogued simplification of the 246.7(g) anchors)

  7. Assembles + persists the determination input snapshot (ADR-028 / T2-4): the participant’s categorical+income rules_input + its ruleset output, the gate results, the assigned food package, the resolved policy params, and the ruleset corpus-hash (captured per participant). WIC carries no itemized fact arrays, so the snapshot’s fact record is the participant set. Its SHA-256 (RFC 8785 canonical) becomes the snapshot_hash signed into the determination; the snapshot is stored immutably in determination_snapshots in the same transaction. WIC is non-FTI, so (unlike tanf/medicaid) the snapshot does not join the ADR-014 chain.

  8. Signs the determination with ECDSA P-256, person_id set before signing (ADR-002 / ADR-035 MR1) — the signature now covers snapshot_hash

All N per-participant determinations + snapshots + (conditional) participant records are persisted + their events staged in one transaction (all-or-nothing). An empty participants[] list — or a duplicate person_id — returns 422.

Response (200): DeterminationList — one signed SignableDetermination envelope per participant (ADR-035), each carrying its person_id and the snapshot_hash binding the input snapshot (ADR-028); per ADR-002 (status, eligible category, food package, certification period, basis, JWS signature; WIC-specific fields under program_extension). The orchestrator receives only outcome + hash, never the snapshot cleartext.

Orchestrated dispatch (#769): until the worker-fact corpus (epic &56 / #858), the orchestrator cannot source WIC’s worker-facts (participant category, breastfeeding status, nutritional risk), so map_wic_context returns a structured input_unsatisfiable result naming those facts rather than dispatching — a strict improvement over the prior silent 422. Direct service-token callers exercise the per-participant path today.

GET /v1/determinations

List determinations for a household.

Minimum role: caseworker.

Query parameters: household_id (required).

Response (200): array of WicDeterminationRead (each the WicDetermination fields flattened, including the required snapshot_hash — #911 retired the snapshot_status marker with the ADR-028 §58 legacy backstop).

GET /v1/determinations/{id}

Fetch a determination by ID.

Minimum role: caseworker.

Response (200): WicDeterminationRead — the WicDetermination fields flattened, including the required snapshot_hash (#911: legacy pre-snapshot rows were deleted and the ADR-028 §58 snapshot_status marker retired). Returns 404 if no determination exists for the supplied ID.

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 — a dedicated, privileged, irreversible role for redaction/expungement. Admins do NOT auto-hold it (separation of duties, 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 snap’s reference impl), not the AIP-136 custom-method …/{id}:redact — axum/matchit 0.8 allows only one parameter per path segment.

Participants

GET /v1/participants/{id}

Fetch an active WIC participant record — current category, certification period, assigned food package.

Minimum role: caseworker.

Nutritional Risk Assessment

WIC certification cannot complete without a recorded clinical nutritional risk assessment. The assessment is clinically determined (anthropometric + biochemical + dietary + medical history) and recorded here — canopy-wic does not compute risk, it stores the clinician’s determination and gates certification on its presence.

POST /v1/nutritional-risk-assessments

Record a nutritional risk assessment for a person.

Minimum role: caseworker.

Request:

{
  "person_id": "uuid",
  "assessed_at": "2026-05-01",
  "assessed_by": "uuid",
  "risk_codes": ["201", "211"],
  "anthropometric": { "height_cm": 158, "weight_kg": 65, "head_circumference_cm": null },
  "biochemical": { "hemoglobin_g_dl": 11.2 },
  "dietary_findings": "low iron intake",
  "follow_up_required": true
}

risk_codes are the WIC nutritional risk codes (USDA FNS-defined). Any non-empty list gates certification ON; an explicit "no risk" assessment uses an empty risk_codes array.

Response (201): the persisted assessment record.

GET /v1/nutritional-risk-assessments

List assessments for a person.

Minimum role: caseworker.

Query parameters: person_id (required).

GET /v1/nutritional-risk-assessments/{id}

Fetch a single assessment.

Minimum role: caseworker.

Certification Appointments (#448)

Backs the worker-portal #392 BFF action handler actions_wic::schedule_certification_appointment_wic.

POST /v1/wic/certifications/{id}/appointments

Schedule a certification (initial / recertification / midcert follow-up) appointment. The path id is a participant UUID — canopy-wic doesn’t have a dedicated certifications table; certification windows live on wic_participants.certification_start/end. The handler looks up the participant’s household and writes the appointment row. Falls back to using the participant UUID as household_id when the participant lookup misses, so worker-portal scheduling isn’t blocked by stale test data.

Minimum role: service-class caller.

Request:

{
  "appointment_at": "2026-06-15T10:30:00Z",
  "appointment_type": "recertification",
  "notes": "Bring most recent income statement",
  "scheduled_by": "jane.doe"
}

appointment_type values: initial / recertification / midcert_followup.

Response (200): the inserted WicAppointment row.

Worker-Dashboard Feeds (#521)

GET /v1/wic/appointments/upcoming?days={n}

Feed for the worker-dashboard "Upcoming appointments" panel (#521). Lists wic_appointments rows whose appointment_at falls within the next n days (default 30) and whose status = 'scheduled'. Ordered ascending so the soonest appointment surfaces first in the panel.

Minimum role: caseworker.

Query parameters: days (optional, defaults to 30).

Response (200): array of WicAppointment rows.

Error Codes

Code Meaning

400

Invalid input (missing required field, malformed JSON, etc.)

401

Missing or invalid JWT

403

Insufficient role (e.g. redact requires data_steward)

404

Determination, participant, or assessment not found

Events Published

  • wic.determination_completed — determination completed (IDs and status only per ADR-004)

  • wic.certification_created — participant certified (IDs + category + dates only — no clinical data)

  • determination.redacted (T2-6 #687) — a data steward crypto-shredded a determination’s snapshot; carries the actor sub + reason, no plaintext

Edit this page · default