Data Export API (FOIA, audit, portability)
On this page
Overview
Three bulk-export endpoints satisfy data-disclosure obligations:
-
GET /v1/export/audit-events(canopy-security) — admin audit dump covering Pub 1075 §9 access-log review and CMS-aligned audit-trail requests. No PII concerns; the audit log itself is metadata about who read what. -
GET /v1/export/persons(canopy-persons) — FOIA disclosure (default) or citizen-data-portability (mode=portability). Two distinct disclosure shapes; see Person-record FOIA redaction. -
GET /v1/export/determinations(canopy-snap) — bulk SNAP determinations for federal reporting cross-checks and quality-control case sampling.
All three require the admin role (the plan specifies "admin or
quality_control"; the QC role is not yet implemented — it’ll OR-into
the existing guard when added). Each export call publishes a
*.export.requested event so the export operation itself is captured
in the audit chain — the wildcard subscriber persists it like any
other audit event.
Person-record FOIA redaction
mode=foia (default) projects the Person record to a public-records-
safe shape:
| Field | Released? | Rationale |
|---|---|---|
|
✓ |
Names appear in court orders / public records; not exempt under the Georgia Open Records Act personal-privacy carve-outs. |
|
✓ (year only) |
Full DOB is exempt under the personal-privacy carve-out; year-only is the standard de-identification for actuarial / demographic disclosure. |
|
✓ |
Not personally identifying. |
|
✓ |
Operational metadata, not PII. |
|
✗ |
Personal-privacy exemption (Georgia OCGA 50-18-72(a)(20) and
HIPAA-adjacent disability/race fields). The |
mode=portability&person_id=<uuid> is the citizen-data-portability
shape: full record returned for the named data subject only. Used when
an applicant exercises their right to receive their own data — not for
public records requests.
mode=portability requires person_id (no bulk portability dump).
Address join
Both modes return the person’s active addresses joined to the record (#347, shipped 2026-05-04):
| Field | FOIA released? | Rationale |
|---|---|---|
|
✓ |
City/state/zip survive at the FOIA level — these are part of the public address record (voter registration, court filings) and the county FIPS code is jurisdictional metadata, not personal. |
|
✗ |
Street address is exempt under the personal-privacy carve-out; an attacker combining street + birth_year + name would have material to re-identify. |
|
✗ |
Timing-sensitive metadata that fingerprints a record even after PII is removed (e.g. a unique move-in date). |
|
✗ (FOIA only) |
Implementation IDs / audit timestamps. Released in portability mode where the data subject is entitled to their full record. |
In portability mode every Address column is released — full street, unit number, dates, and the address row’s identifier. Callers can re-issue the same export and detect address changes by ID.
JSON vs CSV shape for multi-address persons
Per the issue’s AC 2 design decision:
-
JSON: addresses nest as
addresses: […]on the person object. A person with N addresses gets an N-element array; a person with no active addresses gets an empty array. JSON consumers traverse via the nested path and don’t need to dedupe byperson.id. -
CSV: flattened to one row per
(person, address)pair. Person columns repeat across that person’s address rows. A person with zero addresses still emits one row with empty trailing address columns — we never lose person data from the export, even when the address join is empty. CSV consumers that want one person row shouldGROUP BYonidafter import.
Audit-event export
GET /v1/export/audit-events returns the raw audit chain within the
caller-specified window. No redaction: the audit log is metadata about
who accessed what, not the protected data itself.
Expected callers: federal auditors during the annual Pub 1075 §9 review, CMS for HIPAA breach analysis, internal QC for compliance investigations.
Determinations export
GET /v1/export/determinations returns SNAP determinations finalized
within the requested window. Includes benefit_amount, status,
signature (the signed JWS proving determination authenticity per
ADR-002), and program_service_version.
Expected callers: FNS-QC sampling, federal reporting cross-checks against canopy-reporting’s FNS-388 / FNS-7176 outputs, internal audit sampling.
Determinations carry no SSN or DOB and are not subject to the Person- record FOIA redaction calculus. Callers combining determinations with PII for cross-program reporting are responsible for downstream redaction.
Common request shape
All three endpoints accept the same query parameters:
| Param | Meaning |
|---|---|
|
Inclusive start of the window (RFC 3339 / ISO 8601). Defaults to 24
hours before |
|
Exclusive end of the window. Defaults to "now". |
|
|
|
Row cap. Default 10 000, hard cap 50 000. Consumers needing more
paginate by re-querying with a different |
|
|
|
Required when |
Responses
-
200 OKwith body in the requested format. CSV responses carry aContent-Disposition: attachment; filename="…"header for browser-driven exports. -
400 Bad Requestfor invalid time windows (from >= to) or missingperson_idin portability mode. -
403 Forbiddenfor non-admin callers.
Audit of the export
Every export call publishes one of:
-
audit.export.requested(canopy-security) -
persons.export.requested(canopy-persons; carriesmodefield andperson_idin portability mode) -
snap.export.requested(canopy-snap)
with payload fields actor, from, to, format, row_count. The
canopy-security wildcard subscriber persists each event into the
audit chain. Querying the same chain to trace export activity is
itself an export — recursive but consistent.
A failure to publish the *.export.requested event is logged at WARN
level but does not fail the request: the caller is already
authenticated as an admin, the data is visible elsewhere, and the
persisted chain still holds the original audit events. Losing the
per-export audit row is degraded-but-not-catastrophic.
Implementation references
-
services/canopy-security/src/api/export.rs -
services/canopy-persons/src/api/export.rs -
services/canopy-snap/src/api/export.rs -
Plan: Operational Infrastructure — Step 11