This is a personal proof-of-concept project. It is not intended for production use. Please refrain from using it.
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