Compliance Reports API
Endpoints for generating, listing, downloading, and verifying regulatory PDF reports (EU AI Act Article 12 / 9 / 6).
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /compliance/reports | Generate a new report |
| GET | /compliance/reports | List reports |
| GET | /compliance/reports/{id} | Get a report's metadata |
| GET | /compliance/reports/{id}/download | Download the PDF |
| GET | /compliance/reports/{id}/verify | Verify the report's signature |
| GET | /actions/{id}/explanation | Article 6 explanation as JSON |
| GET | /actions/{id}/explanation/pdf | Article 6 explanation as PDF |
| POST | /verify/explanation | Verify a signed explanation envelope (public, no auth) |
All endpoints require a Bearer API key. The route group is gated by the
ENABLE_COMPLIANCE_REPORTS setting; when disabled all endpoints return 404.
Supported frameworks
| Framework | Regulatory anchor | Required inputs | Output |
|---|---|---|---|
eu_ai_act_art12 | Article 12 / Annex VII | period | Technical file — every receipt in the period with Annex VII field mapping |
eu_ai_act_art9 | Article 9 | period | Risk register — actions bucketed by Annex III high-risk category |
eu_ai_act_art6 | Article 6 / Article 13 | action_uuid | Right-to-explanation for a single action |
eu_ai_act_annex_iv | Annex IV (referenced by Article 11) | period | Full technical documentation (§§1..9): provider description, design elements, capabilities, risk management cross-reference, lifecycle, standards, EU declaration of conformity, post-market monitoring, conformity record. 10-year retention. |
Generate a report
POST /api/v1/compliance/reportsRequest body
| Field | Type | Required | Description |
|---|---|---|---|
framework | string | yes | One of eu_ai_act_art12, eu_ai_act_art9, eu_ai_act_art6, eu_ai_act_annex_iv |
period_start | datetime | for art12/art9/annex_iv | ISO 8601 UTC |
period_end | datetime | for art12/art9/annex_iv | ISO 8601 UTC. Must be ≥ period_start |
action_uuid | string (UUID) | for art6 | The action to explain |
agent_filter | string[] | no | Restrict the period selection to specific agent IDs |
Response
{
"id": "ce8b0fa1-...",
"framework": "eu_ai_act_art12",
"status": "ready",
"org_uuid": "...",
"period_start": "2026-04-01T00:00:00",
"period_end": "2026-04-30T23:59:59",
"action_uuid": null,
"agent_filter": null,
"receipt_count": 1234,
"pdf_size_bytes": 28471,
"content_hash": "sha256:...",
"signature": "ed25519:...",
"signing_key_id": "aira-signing-key-v1",
"timestamp_token": "...",
"timestamp_token_present": true,
"report_metadata": { ... },
"error_message": null,
"generated_at": "2026-04-30T...",
"created_at": "2026-04-30T...",
"request_id": "req_..."
}The PDF is not in the response. Fetch it from the download endpoint.
Errors
| Code | When |
|---|---|
400 UNSUPPORTED_FRAMEWORK | Framework is not one of the supported values |
400 INVALID_PERIOD | Period start/end missing or end < start (Article 12, Article 9) |
400 INVALID_ACTION | action_uuid missing for Article 6 |
400 AGENT_FILTER_TOO_LARGE | agent_filter has more than 200 items |
400 REPORT_TOO_LARGE | Period would include more than 100,000 receipts. Split into smaller windows or use a Compliance Bundle (which scales by Merkle commitment instead of per-receipt rendering). |
| 401 | Missing or invalid Bearer token |
404 ACTION_NOT_FOUND | Article 6 action does not exist or belongs to another org |
| 429 | Per-org rate limit exceeded (10 reports/minute) |
List reports
GET /api/v1/compliance/reportsQuery params:
| Param | Type | Description |
|---|---|---|
framework | string | Filter by framework |
status | string | Filter by pending, generating, ready, failed |
limit | int (1-200) | Page size. Defaults to 50 when omitted. |
offset | int | Number of items to skip. Defaults to 0. |
Returns { items, total, limit, offset, request_id }.
total reflects the full count matching the filters (not just the
items in the current page) — paginate by incrementing offset until
offset + items.length >= total.
Get a report
GET /api/v1/compliance/reports/{report_uuid}Returns the same shape as the create response.
Download a report
GET /api/v1/compliance/reports/{report_uuid}/downloadReturns the PDF as application/pdf. The response sets
Content-Disposition: inline; filename="aira-{framework}-{id}.pdf",
so clicking the URL in a browser opens the PDF in a new tab instead
of forcing a download. Both framework and id segments are sanitized
to [A-Za-z0-9._-] before being inserted into the header.
Other headers:
X-Aira-Content-Hash— SHA-256 of the PDF bytes (sha256:HEX)X-Aira-Signature— Ed25519 signature (ed25519:BASE64URL)X-Aira-Signing-Key— Public key identifier (look up at/.well-known/jwks.json)
Rate-limited per org (60 requests/minute). The Python and TypeScript SDKs retry transparently on 5xx (3 attempts, exponential backoff).
Errors:
- 404
REPORT_NOT_READYif the report is still pending/generating/failed. - 429 if the per-org download rate limit is exceeded.
Verify a report
GET /api/v1/compliance/reports/{report_uuid}/verifyRecomputes the SHA-256 of the stored PDF bytes and verifies the Ed25519 signature against the descriptor that was signed at generation time. Returns:
{
"report_uuid": "...",
"valid": true,
"checks": {
"content_hash_matches": true,
"signature_valid": true
},
"descriptor": { ... },
"request_id": "req_..."
}valid is true iff every entry in checks is true.
Article 6 explanation (single action)
JSON
GET /api/v1/actions/{action_uuid}/explanationReturns:
{
"action": {
"id": "...",
"agent_id": "...",
"action_type": "...",
"input_hash": "sha256:...",
"output_hash": "sha256:...",
...
},
"policy_chain": [
{
"evaluation_uuid": "...",
"policy_name": "...",
"mode": "rules" | "ai" | "consensus" | "content_scan",
"decision": "ALLOW" | "DENY" | "REVIEW",
"confidence": 0.94,
"reasoning": "...",
"model_votes": { ... },
"signature": "ed25519:...",
"evaluated_at": "..."
}
],
"approval_chain": [ ... ],
"receipt": { ... },
"regulation": {
"framework": "eu_ai_act",
"articles": ["Article 6", "Article 13", "Article 14"]
},
"_envelope": {
"alg": "Ed25519",
"signing_key_id": "aira-signing-key-v1",
"content_hash": "sha256:...",
"signature": "ed25519:...",
"generated_at": "2026-04-12T00:00:00Z"
},
"request_id": "req_..."
}The _envelope block is an Ed25519 signature over the canonical JSON
of every other field above. _envelope itself and request_id are
excluded from the signed payload — so exporting the JSON, stripping
request_id, and re-verifying round-trips cleanly. Content hash is
sha256 of the canonical payload; the public key can be fetched from
/.well-known/jwks.json.
GET /api/v1/actions/{action_uuid}/explanation/pdfStreams the explanation as application/pdf. Same custom headers as the
report download endpoint. The PDF also renders the _envelope block so
the paper copy carries the same signature info as the JSON form.
Verify an explanation envelope (public)
POST /api/v1/verify/explanationNo auth required. A regulator or auditor holding a saved JSON export can re-derive the canonical hash and verify the Ed25519 signature against the public JWKS, without holding an Aira API key.
Rate-limited per IP.
Request body
{
"explanation": {
"action": { ... },
"policy_chain": [ ... ],
"approval_chain": [ ... ],
"receipt": { ... },
"regulation": { ... },
"_envelope": {
"alg": "Ed25519",
"signing_key_id": "aira-signing-key-v1",
"content_hash": "sha256:...",
"signature": "ed25519:...",
"generated_at": "..."
}
}
}Send the full JSON that GET /actions/{id}/explanation returned.
request_id and _envelope are excluded from the canonical signed
payload, so including them in the request body is fine — the server
strips them before recomputing the hash.
Response
{
"valid": true,
"checks": {
"key_known": true,
"content_hash_matches": true,
"signature_valid": true
},
"signing_key_id": "aira-signing-key-v1",
"request_id": "req_..."
}valid is true iff every entry in checks is the literal boolean
true. On failure the matching check is a string describing the
specific problem — render it directly to the reader. The three checks:
| Check | Meaning |
|---|---|
key_known | signing_key_id found in the signing keys table (also published via /.well-known/jwks.json) |
content_hash_matches | sha256 of the canonical JSON (excluding _envelope + request_id) equals _envelope.content_hash |
signature_valid | Ed25519 signature verifies against the public key |
Errors
| Code | When |
|---|---|
| 404 | Compliance reports feature flag is disabled on this deployment |
| 429 | Per-IP rate limit exceeded |
Envelope problems never cause a 4xx — the endpoint always returns a
structured { valid, checks } body so the caller can render a
meaningful diagnosis (missing envelope, unknown key, tampered
payload, bad signature).