Aira

Compliance Reports API

Endpoints for generating, listing, downloading, and verifying regulatory PDF reports (EU AI Act Article 12 / 9 / 6).

Endpoints

MethodPathDescription
POST/compliance/reportsGenerate a new report
GET/compliance/reportsList reports
GET/compliance/reports/{id}Get a report's metadata
GET/compliance/reports/{id}/downloadDownload the PDF
GET/compliance/reports/{id}/verifyVerify the report's signature
GET/actions/{id}/explanationArticle 6 explanation as JSON
GET/actions/{id}/explanation/pdfArticle 6 explanation as PDF
POST/verify/explanationVerify 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

FrameworkRegulatory anchorRequired inputsOutput
eu_ai_act_art12Article 12 / Annex VIIperiodTechnical file — every receipt in the period with Annex VII field mapping
eu_ai_act_art9Article 9periodRisk register — actions bucketed by Annex III high-risk category
eu_ai_act_art6Article 6 / Article 13action_uuidRight-to-explanation for a single action
eu_ai_act_annex_ivAnnex IV (referenced by Article 11)periodFull 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/reports

Request body

FieldTypeRequiredDescription
frameworkstringyesOne of eu_ai_act_art12, eu_ai_act_art9, eu_ai_act_art6, eu_ai_act_annex_iv
period_startdatetimefor art12/art9/annex_ivISO 8601 UTC
period_enddatetimefor art12/art9/annex_ivISO 8601 UTC. Must be ≥ period_start
action_uuidstring (UUID)for art6The action to explain
agent_filterstring[]noRestrict 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

CodeWhen
400 UNSUPPORTED_FRAMEWORKFramework is not one of the supported values
400 INVALID_PERIODPeriod start/end missing or end < start (Article 12, Article 9)
400 INVALID_ACTIONaction_uuid missing for Article 6
400 AGENT_FILTER_TOO_LARGEagent_filter has more than 200 items
400 REPORT_TOO_LARGEPeriod 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).
401Missing or invalid Bearer token
404 ACTION_NOT_FOUNDArticle 6 action does not exist or belongs to another org
429Per-org rate limit exceeded (10 reports/minute)

List reports

GET /api/v1/compliance/reports

Query params:

ParamTypeDescription
frameworkstringFilter by framework
statusstringFilter by pending, generating, ready, failed
limitint (1-200)Page size. Defaults to 50 when omitted.
offsetintNumber 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}/download

Returns 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_READY if 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}/verify

Recomputes 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}/explanation

Returns:

{
  "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.

PDF

GET /api/v1/actions/{action_uuid}/explanation/pdf

Streams 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/explanation

No 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:

CheckMeaning
key_knownsigning_key_id found in the signing keys table (also published via /.well-known/jwks.json)
content_hash_matchessha256 of the canonical JSON (excluding _envelope + request_id) equals _envelope.content_hash
signature_validEd25519 signature verifies against the public key

Errors

CodeWhen
404Compliance reports feature flag is disabled on this deployment
429Per-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).

On this page