{"openapi":"3.0.3","info":{"title":"AXIOM Protocol API","version":"4.0.0","description":"Signed audit receipts for AI data access. Every logged access is hashed (SHA-256 over the canonical record), signed (RSA-PSS/SHA-256 — the v4 receipt model), and stored; receipt signatures are independently verifiable with the published public key. Batches of receipts can be anchored to the **Solana devnet** via a Merkle root; anchoring runs daily via an external scheduler (a GitHub Actions cron calling a secret-protected trigger) — GET /v1/anchor/status reports what has actually run.\n\n**Authentication.** All endpoints require the `x-axiom-key` API key header, except: GET /v1/health, GET /.well-known/axiom-public-key.pem, GET /v1/verify/{receiptId}, GET /openapi.json, GET /docs, POST /v1/signup, and GET /v1/signup/verify (public), and POST /v1/anchor/run (protected by the separate `x-anchor-secret` header instead).\n\n**Errors.** Every 4xx/5xx response is a JSON envelope `{error, code, message, field?}` — `error` is a coarse category, `code` is a stable machine code from a fixed append-only registry (see the ErrorEnvelope schema). Unknown routes answer 404 `ROUTE_NOT_FOUND`; malformed JSON bodies answer 400 `INVALID_JSON`; oversized bodies answer 413 `PAYLOAD_TOO_LARGE`. A small number of legacy anchor error responses omit `code` (noted per endpoint).\n\n**Rate limiting.** A global limit of 1000 requests/minute per client IP applies to all routes (429 `RATE_LIMITED`, with standard `RateLimit-*` headers). `/v1/log` and `/v1/log/batch` pass an additional 10000/minute limiter. Every authenticated route additionally enforces a PER-KEY sliding-window limit against that tenant's own `rate_limit_rpm` (see GET /v1/me) — self-serve keys default to 60 requests/minute, far tighter than existing partner keys, so unvetted public traffic cannot degrade service for other tenants. POST /v1/signup and GET /v1/signup/verify each have their OWN separate, stricter per-IP limiter, since both are reachable with no API key at all: POST /v1/signup is limited to 5 requests/hour/IP; GET /v1/signup/verify (reached by clicking an emailed link, not a form submit — a different traffic pattern) has a separate, more generous 30 requests/hour/IP limiter. Note: a batch call currently counts as 1 request against the limiter even though it can carry up to 100 receipts (documented gap).\n\n**Request IDs.** Every response carries an `X-Request-Id` header for support correlation.\n\nOperated by 9950931 Canada Inc.","contact":{"name":"9950931 Canada Inc."},"license":{"name":"BUSL-1.1 (Business Source License 1.1 — source-available, proprietary use restrictions apply)"}},"servers":[{"url":"https://axiom-protocol.onrender.com"}],"tags":[{"name":"Health","description":"Service liveness (public)."},{"name":"Keys","description":"Public signing key for independent receipt verification (public)."},{"name":"Signup","description":"Self-serve signup: request access and verify email to receive an API key (public, no pricing). Instant key issuance, no public pricing — request-access framing."},{"name":"Logging","description":"Create signed access receipts — single and batch, with idempotency."},{"name":"Verification","description":"Public receipt verification — no API key required."},{"name":"Transactions","description":"Tenant-scoped receipt listing and introspection."},{"name":"Access","description":"Access-grant management (original /v1/access surface)."},{"name":"Grants","description":"Access-grant management with signed lifecycle receipts (/v1/grants surface)."},{"name":"Alerts","description":"Compliance alerts raised by the inline compliance engine."},{"name":"Anchoring","description":"Solana devnet anchoring: status, per-receipt Merkle proofs, and the secret-protected external trigger. Anchoring runs daily via an external scheduler (GitHub Actions cron) calling that trigger."},{"name":"Tenant","description":"Tenant self-introspection."},{"name":"API Keys","description":"Tenant self-service API key visibility and revocation (distinct from the 'Keys' tag, which is the public RSA signing key for receipt verification)."},{"name":"Webhooks","description":"HMAC-signed alert webhooks (one per tenant)."},{"name":"Documentation","description":"This specification and its rendered reference page (public)."}],"security":[{"ApiKeyAuth":[]}],"paths":{"/v1/health":{"get":{"tags":["Health"],"summary":"Service health","description":"Public liveness check: verifies database connectivity. Returns no per-tenant or aggregate counts.","security":[],"responses":{"200":{"description":"Service healthy.","content":{"application/json":{"schema":{"type":"object","required":["status","version","database","database_time","timestamp"],"properties":{"status":{"type":"string","enum":["healthy"]},"version":{"type":"string","example":"4.0.0"},"database":{"type":"string","enum":["connected"]},"database_time":{"type":"string","description":"Current timestamp reported by the database."},"timestamp":{"type":"string","format":"date-time"}}}}}},"503":{"description":"Database unreachable.","content":{"application/json":{"schema":{"type":"object","required":["status","error","timestamp"],"properties":{"status":{"type":"string","enum":["unhealthy"]},"error":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}}}}}},"/.well-known/axiom-public-key.pem":{"get":{"tags":["Keys"],"summary":"Axiom Protocol public signing key (PEM)","description":"Public, unauthenticated: anyone can fetch the RSA public key and verify receipt signatures (RSA-PSS/SHA-256 over record_hash) — the signature check itself runs locally once you have the key. The key is served from this same API host, so pin the PEM out-of-band if your threat model includes the operator. Served with Cache-Control max-age=300 so a key rotation propagates within minutes.","security":[],"responses":{"200":{"description":"PEM-encoded RSA public key.","headers":{"X-Key-Fingerprint":{"description":"Stable key fingerprint: 'sha256:<hex>' over the canonical SPKI DER.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=300","schema":{"type":"string"}}},"content":{"text/plain":{"schema":{"type":"string","example":"-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"}}}}}}},"/v1/signup":{"post":{"tags":["Signup"],"summary":"Request self-serve API access","description":"Public — no API key required. Submits an email for verification; a verification link is emailed via Resend (24-hour expiry). The response is IDENTICAL regardless of whether this email already has a pending or verified signup (anti-enumeration: this endpoint never reveals whether an email is already registered). Has its own strict per-IP rate limit (separate from the global limiter), since it is the first endpoint that lets an anonymous caller trigger a real side effect (an email send, and eventually a real API key). FAILS CLOSED with 503 if the email backend (RESEND_API_KEY) is not configured — no signup row is written and no email is claimed to have been sent in that case. No pricing or billing information is collected or returned anywhere in this flow (request-access framing).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":320,"description":"Address to verify and, on success, receive the API key at."}}}}}},"responses":{"200":{"description":"Always the same generic message — never reveals prior signup state for this email.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}},"400":{"description":"Missing or malformed email. Codes: MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Per-IP signup rate limit exceeded: max 5 requests/hour/IP (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Signup request could not be processed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Self-serve signup is temporarily unavailable because the email backend is not configured (SIGNUP_UNAVAILABLE). Fail-closed: no signup row is written and no email is claimed to have been sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/signup/verify":{"get":{"tags":["Signup"],"summary":"Verify email and receive the issued API key","description":"Public — no API key required. Looks up the emailed token; on the FIRST successful verification, issues a new 'axm_live_sk_'-prefixed API key (rate_limit_rpm 60 — a conservative default for unvetted public traffic) and returns the RAW key EXACTLY ONCE. Idempotent-safe: re-clicking an already-verified link never issues a second key (400 SIGNUP_ALREADY_VERIFIED instead). No pricing or billing language is returned anywhere in this response.","security":[],"parameters":[{"name":"token","in":"query","required":true,"description":"The verification token from the emailed link.","schema":{"type":"string"}}],"responses":{"200":{"description":"Email verified and API key issued. STORE THE KEY NOW — it is never shown again.","content":{"application/json":{"schema":{"type":"object","required":["message","api_key","key_id","rate_limit_rpm","quickstart","note"],"properties":{"message":{"type":"string"},"api_key":{"type":"string","description":"'axm_live_sk_' + 32 hex characters. Returned only in this response; not retrievable later."},"key_id":{"type":"string","description":"Public (non-secret) key identifier."},"rate_limit_rpm":{"type":"integer","enum":[60]},"quickstart":{"type":"object","properties":{"curl_example":{"type":"string"},"docs_url":{"type":"string"}}},"note":{"type":"string","example":"store this; not retrievable later"}}}}}},"400":{"description":"Codes: MISSING_REQUIRED_FIELD (no token), SIGNUP_TOKEN_EXPIRED, SIGNUP_ALREADY_VERIFIED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No signup matches this token (SIGNUP_TOKEN_INVALID).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Per-IP rate limit exceeded: max 30 requests/hour/IP (RATE_LIMITED). A separate, more generous limiter than POST /v1/signup — this endpoint is reached by clicking an emailed link, not submitting the form.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Verification could not be completed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/log":{"post":{"tags":["Logging"],"summary":"Log one access and get a signed receipt","description":"Validates the access record, signs it (RSA-PSS/SHA-256), stores it, and runs the inline compliance check against this tenant's active grants. Optional idempotency via the Idempotency-Key header: retrying with the same key and a materially identical body returns the ORIGINAL response verbatim (200 + X-Idempotent-Replay: true); the same key with a materially different body answers 409 IDEMPOTENCY_KEY_REUSE. Idempotency keys never expire.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional dedupe key, 1-255 printable ASCII characters, scoped to your tenant. Malformed keys answer 400 IDEMPOTENCY_KEY_INVALID.","schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogRequest"}}}},"responses":{"200":{"description":"Idempotent replay: the ORIGINAL 201 response body, byte-identical (same receipt_id, signature, verification_url), served with X-Idempotent-Replay: true. Nothing new was created.","headers":{"X-Idempotent-Replay":{"description":"Always 'true' on a replay.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Receipt"}}}},"201":{"description":"Receipt created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Receipt"}}}},"400":{"description":"Validation failure. Codes: BODY_NOT_OBJECT, MISSING_REQUIRED_FIELD, DATA_ACCESSED_EMPTY, DATA_ACCESSED_ITEM_INVALID, INVALID_TIMESTAMP, EXPIRES_AT_IN_PAST, PURPOSE_INVALID, AUTHORIZATION_METHOD_INVALID, IDEMPOTENCY_KEY_INVALID, INVALID_JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Idempotency-Key already used with a materially different body (IDEMPOTENCY_KEY_REUSE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Receipt could not be stored (LOG_FAILED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/log/batch":{"post":{"tags":["Logging"],"summary":"Log up to 100 accesses in one call","description":"Each item is validated and processed by EXACTLY the same code path as single /v1/log (compliance check included, per item). Body is a bare JSON array of log bodies or {\"receipts\": [...]}. Partial success is normal: the call answers 200 whenever the batch itself was well-formed, and results[] carries a per-item status in input order. Per-item idempotency uses the item field 'idempotency_key' (same storage and semantics as the single-endpoint header, so the two endpoints dedupe against each other); the Idempotency-Key HEADER is rejected with 400 BATCH_HEADER_IDEMPOTENCY_UNSUPPORTED rather than silently ignored. Items are processed sequentially; one failing item never aborts the rest. Note: a batch currently counts as 1 request against the rate limiter even though it carries up to 100 receipts (documented gap).","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/LogRequestBatchItem"},"minItems":1,"maxItems":100},{"type":"object","required":["receipts"],"properties":{"receipts":{"type":"array","items":{"$ref":"#/components/schemas/LogRequestBatchItem"},"minItems":1,"maxItems":100}}}]}}}},"responses":{"200":{"description":"Batch processed (individual items may still have failed — check results[].status).","content":{"application/json":{"schema":{"type":"object","required":["results","count","succeeded","failed"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/BatchResultItem"},"description":"One entry per input item, in input order."},"count":{"type":"integer","description":"Total items processed."},"succeeded":{"type":"integer","description":"Items with status 200 or 201."},"failed":{"type":"integer"}}}}}},"400":{"description":"Batch-level failure. Codes: BATCH_BODY_INVALID, BATCH_EMPTY, BATCH_TOO_LARGE, BATCH_HEADER_IDEMPOTENCY_UNSUPPORTED, INVALID_JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"Request body exceeds the parser size limit (PAYLOAD_TOO_LARGE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Batch could not be processed (LOG_FAILED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/verify/{receiptId}":{"get":{"tags":["Verification"],"summary":"Publicly verify a receipt","description":"Public, unauthenticated. Returns the stored hash and signature, the live signature-verification result, and step-by-step instructions for independent verification with the published public key. Anchoring is reported by what is actually true for THIS record — anchor.anchored is false until the record's batch has genuinely been anchored to Solana devnet.","security":[],"parameters":[{"name":"receiptId","in":"path","required":true,"schema":{"type":"string"},"description":"Receipt ID (axm_rec_...)."}],"responses":{"200":{"description":"Verification result.","content":{"application/json":{"schema":{"type":"object","required":["receipt_id","record_hash","axiom_signature","signature_valid","axiom_public_key_url","signed_at","record_summary","anchor","verification_instructions"],"properties":{"receipt_id":{"type":"string"},"record_hash":{"type":"string","description":"'sha256:<hex>'."},"axiom_signature":{"type":"string","description":"'axiom_sig_v1:<base64>'."},"signature_valid":{"type":"boolean","description":"Live RSA-PSS/SHA-256 verification of the stored signature against the stored hash."},"axiom_public_key_url":{"type":"string","description":"Where to fetch the public key for independent verification."},"signed_at":{"type":"string","format":"date-time"},"record_summary":{"type":"object","description":"Summary only — the full original submission is available to its owning tenant via GET /v1/transactions/{id}.","properties":{"model_id":{"type":"string"},"authorized_by":{"type":"string"},"purpose":{"type":"string"},"data_classifications":{"type":"array","items":{"type":"string"}},"expires_at":{"type":"string"}}},"anchor":{"type":"object","required":["anchored","message"],"properties":{"anchored":{"type":"boolean","description":"True only when this record's batch was actually anchored (Solana devnet)."},"message":{"type":"string"}}},"verification_instructions":{"type":"string","description":"Newline-separated steps to verify the signature independently."}}}}}},"404":{"description":"No record with that receipt ID (RECEIPT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Verification failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/transactions":{"get":{"tags":["Transactions"],"summary":"List this tenant's receipts (filterable, cursor-paged)","description":"Strict filter validation: any query parameter NOT listed below answers 400 UNSUPPORTED_FILTER (never silently ignored); a parameter supplied more than once (?p=a&p=b) or in bracket form (?p[x]=y) answers 400 INVALID_FILTER_VALUE; empty-string values are treated as not provided. Results are ordered newest-first. Paging: pass the response's cursor back as ?cursor= to fetch the next page while has_more is true.","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only receipts with timestamp >= this ISO-8601 instant. Unparseable dates answer 400 INVALID_FILTER_VALUE."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only receipts with timestamp <= this ISO-8601 instant."},{"name":"model_id","in":"query","required":false,"schema":{"type":"string"},"description":"Exact, case-sensitive match."},{"name":"authorized_by","in":"query","required":false,"schema":{"type":"string"},"description":"Exact, case-sensitive match."},{"name":"purpose","in":"query","required":false,"schema":{"type":"string","enum":["diagnostic_assistance","fraud_detection","content_moderation","recommendation","data_analysis","autonomous_action","research","other"]},"description":"Case-insensitive in; values outside the closed set answer 400 INVALID_FILTER_VALUE."},{"name":"anchor_status","in":"query","required":false,"schema":{"type":"string"},"description":"Lowercased, then exact match (e.g. 'pending_24h_anchor', 'anchored'). Not enum-validated: the status vocabulary can grow with the anchoring pipeline."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000,"default":100},"description":"Page size. Non-integer or out-of-range values answer 400 LIMIT_OUT_OF_RANGE."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque paging cursor from a previous response (a receipt_id); returns receipts strictly 'before' it."}],"responses":{"200":{"description":"One page of receipts.","content":{"application/json":{"schema":{"type":"object","required":["transactions","total","has_more","cursor"],"properties":{"transactions":{"type":"array","items":{"$ref":"#/components/schemas/TransactionSummary"}},"total":{"type":"integer","description":"Number of items in THIS page (not the overall total)."},"has_more":{"type":"boolean"},"cursor":{"type":"string","nullable":true,"description":"Pass back as ?cursor= for the next page; null on the last page."}}}}}},"400":{"description":"Filter validation failure. Codes: UNSUPPORTED_FILTER, INVALID_FILTER_VALUE, LIMIT_OUT_OF_RANGE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Listing failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/transactions/{id}":{"get":{"tags":["Transactions"],"summary":"Fetch one receipt with the full original submission","description":"Tenant-scoped introspection: returns the stored hash/signature/anchor state plus the FULL original submission for one of YOUR receipts. Unlike public /v1/verify, this never crosses tenants — another tenant's receipt ID answers 404.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Receipt ID (axm_rec_...)."}],"responses":{"200":{"description":"Full stored transaction.","content":{"application/json":{"schema":{"type":"object","required":["receipt_id","timestamp","record_hash","signature","anchor_status","anchor_batch","original_submission"],"properties":{"receipt_id":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"record_hash":{"type":"string"},"signature":{"type":"string"},"anchor_status":{"type":"string"},"anchor_batch":{"type":"string"},"original_submission":{"type":"object","properties":{"model_id":{"type":"string"},"model_version":{"type":"string","nullable":true},"data_accessed":{"type":"array","items":{"$ref":"#/components/schemas/DataAccessedItem"}},"authorized_by":{"type":"string"},"authorization_method":{"type":"string"},"purpose":{"type":"string"},"session_id":{"type":"string"},"expires_at":{"type":"string"},"patient_consent_id":{"type":"string","nullable":true},"metadata":{"type":"object"}}}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No such receipt for THIS tenant (TRANSACTION_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Lookup failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/access/grant":{"post":{"tags":["Access"],"summary":"Create an access grant","description":"Creates a signed access grant that the inline compliance engine checks receipts against. The grant record itself is hashed and signed (same v4 signature model as receipts).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ai_system_id","data_categories","classification_ceiling","authorized_by","authorization_basis","purpose","expires_at"],"properties":{"ai_system_id":{"type":"string"},"data_categories":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Resource types this grant covers (matched against data_accessed[].resource_type)."},"classification_ceiling":{"type":"string","enum":["PHI","PII","CONFIDENTIAL","INTERNAL","PUBLIC"],"description":"Highest data classification this grant permits."},"authorized_by":{"type":"string"},"authorization_basis":{"type":"string"},"purpose":{"type":"string","enum":["diagnostic_assistance","fraud_detection","content_moderation","recommendation","data_analysis","autonomous_action","research","other"]},"expires_at":{"type":"string","format":"date-time","description":"Must parse and be in the future (400 INVALID_TIMESTAMP / EXPIRES_AT_IN_PAST otherwise)."},"max_records_per_day":{"type":"integer","nullable":true},"allowed_fields":{"type":"array","items":{"type":"string"}},"denied_fields":{"type":"array","items":{"type":"string"}},"geo_restriction":{"type":"string","nullable":true},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"Grant created.","content":{"application/json":{"schema":{"type":"object","required":["grant_id","grant_hash","signature","created_at","expires_at","status"],"properties":{"grant_id":{"type":"string","example":"grant_9F2C41D08A7B3E65C1D4F0A2B8"},"grant_hash":{"type":"string"},"signature":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string"},"status":{"type":"string","enum":["active"]},"ai_system_id":{"type":"string"},"data_categories":{"type":"array","items":{"type":"string"}},"classification_ceiling":{"type":"string"},"authorized_by":{"type":"string"},"purpose":{"type":"string"}}}}}},"400":{"description":"Validation failure. Codes: MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE, INVALID_TIMESTAMP, EXPIRES_AT_IN_PAST.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Grant could not be created (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/access/revoke":{"post":{"tags":["Access"],"summary":"Revoke a grant (by body field)","description":"Revokes one of this tenant's grants, effective immediately. Free-text context: 'notes' is the canonical field; 'reason_detail' is a documented alias (the field's original name here) — when both are sent, notes wins.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["grant_id","revoked_by","reason"],"properties":{"grant_id":{"type":"string"},"revoked_by":{"type":"string"},"reason":{"type":"string","enum":["anomaly_detected","authorization_expired","policy_change","security_incident","audit_finding","manual_review","other"]},"notes":{"type":"string","description":"Canonical free-text field."},"reason_detail":{"type":"string","description":"Alias for notes (backward compatibility)."}}}}}},"responses":{"200":{"description":"Grant revoked.","content":{"application/json":{"schema":{"type":"object","required":["grant_id","revocation_id","revocation_timestamp","effective_immediately","revoked_by","reason"],"properties":{"grant_id":{"type":"string"},"revocation_id":{"type":"string","description":"'rev_' + epoch milliseconds."},"revocation_timestamp":{"type":"string","format":"date-time"},"effective_immediately":{"type":"boolean","enum":[true]},"revoked_by":{"type":"string"},"reason":{"type":"string"},"reason_detail":{"type":"string","nullable":true,"description":"Same value as notes (legacy name)."},"notes":{"type":"string","nullable":true}}}}}},"400":{"description":"Validation or state failure. Codes: MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE, GRANT_ALREADY_REVOKED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Grant not found for this tenant (GRANT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Revocation failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/access/grants":{"get":{"tags":["Access"],"summary":"List this tenant's grants","description":"Returns full stored grant rows, newest first. Optional filters: ai_system_id (exact match) and status. Like /v1/transactions and /v1/alerts, an unrecognized query parameter is a 400 UNSUPPORTED_FILTER, not a silent no-op.","parameters":[{"name":"ai_system_id","in":"query","required":false,"schema":{"type":"string"},"description":"Exact match."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","revoked"]},"description":"Case-insensitive in, canonical lowercase compare. This is the full, closed set this codebase ever writes to this column."}],"responses":{"200":{"description":"Grant list.","content":{"application/json":{"schema":{"type":"object","required":["grants","total"],"properties":{"grants":{"type":"array","items":{"$ref":"#/components/schemas/GrantRow"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Listing failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/access/grants/{grantId}":{"get":{"tags":["Access"],"summary":"Fetch one grant","description":"Returns the full stored grant row for one of THIS tenant's grants.","parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The stored grant row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantRow"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Grant not found for this tenant (GRANT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Lookup failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/grants":{"post":{"tags":["Grants"],"summary":"Create an access grant (with signed event receipt)","description":"Alternative grant-creation surface: same access_grants storage as /v1/access/grant, but the response embeds a signed RECEIPT recording the creation event in the transactions ledger. Honest validation note: this endpoint enforces required fields, data_categories (non-empty array), classification_ceiling, and expires_at — purpose and authorization_method are stored as given and NOT enum-validated here (unlike /v1/access/grant).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ai_system_id","data_categories","classification_ceiling","authorized_by","authorization_method","purpose","expires_at"],"properties":{"ai_system_id":{"type":"string"},"data_categories":{"type":"array","items":{"type":"string"},"minItems":1},"classification_ceiling":{"type":"string","enum":["PHI","PII","CONFIDENTIAL","INTERNAL","PUBLIC"]},"authorized_by":{"type":"string"},"authorization_method":{"type":"string","description":"Stored as given (not enum-validated on this endpoint)."},"authorization_basis":{"type":"string","nullable":true},"purpose":{"type":"string","description":"Stored as given (not enum-validated on this endpoint)."},"expires_at":{"type":"string","format":"date-time","description":"Must parse and be in the future."},"allowed_fields":{"type":"array","items":{"type":"string"}},"denied_fields":{"type":"array","items":{"type":"string"}},"rate_limits":{"type":"object","nullable":true},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"Grant created, with a signed receipt for the creation event.","content":{"application/json":{"schema":{"type":"object","required":["grant_id","status","ai_system_id","created_at","expires_at","receipt"],"properties":{"grant_id":{"type":"string"},"status":{"type":"string","enum":["active"]},"ai_system_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string"},"receipt":{"$ref":"#/components/schemas/GrantEventReceipt"}}}}}},"400":{"description":"Validation failure. Codes: MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE, INVALID_TIMESTAMP, EXPIRES_AT_IN_PAST.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Grant could not be created (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/grants/{grant_id}/revoke":{"post":{"tags":["Grants"],"summary":"Revoke a grant (by path), with signed event receipt","description":"Revokes the grant and stores a signed receipt of the revocation event. Honest validation note: 'reason' is required but free-text on this endpoint (no enum — unlike /v1/access/revoke). 'notes' is canonical; 'reason_detail' is the documented alias; when provided, notes is also carried in the signed receipt's metadata.","parameters":[{"name":"grant_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason","revoked_by"],"properties":{"reason":{"type":"string","description":"Free text (not enum-validated on this endpoint)."},"revoked_by":{"type":"string"},"notes":{"type":"string","description":"Canonical free-text field."},"reason_detail":{"type":"string","description":"Alias for notes (backward compatibility)."}}}}}},"responses":{"200":{"description":"Grant revoked, with a signed receipt for the revocation event.","content":{"application/json":{"schema":{"type":"object","required":["grant_id","status","revoked_at","notes","receipt"],"properties":{"grant_id":{"type":"string"},"status":{"type":"string","enum":["revoked"]},"revoked_at":{"type":"string","format":"date-time"},"notes":{"type":"string","nullable":true},"receipt":{"$ref":"#/components/schemas/GrantEventReceipt"}}}}}},"400":{"description":"Validation or state failure. Codes: MISSING_REQUIRED_FIELD, GRANT_ALREADY_REVOKED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Grant not found for this tenant (GRANT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Revocation failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/alerts":{"get":{"tags":["Alerts"],"summary":"List compliance alerts (filterable, offset-paged)","description":"Alerts raised by the inline compliance engine when a logged access violated an active grant. Strict filter validation (same contract as /v1/transactions): unknown parameters answer 400 UNSUPPORTED_FILTER; repeated or bracketed parameters answer 400 INVALID_FILTER_VALUE. Enum filters are case-insensitive in, compared lowercase. Note: the engine currently emits only 'critical' and 'high' severities; 'medium' and 'low' are part of the documented scale, so filtering on them is a valid (empty) query.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["open","acknowledged"]}},{"name":"severity","in":"query","required":false,"schema":{"type":"string","enum":["critical","high","medium","low"]}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["unauthorized_data_access","denied_field_accessed","field_not_in_allowlist","access_after_expiry","classification_ceiling_exceeded","volume_exceeded","compliance_check_error"]}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"triggered_at >= this instant."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"triggered_at <= this instant."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":500},"description":"Out-of-range values answer 400 LIMIT_OUT_OF_RANGE."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":1000000,"default":0},"description":"Out-of-range values answer 400 OFFSET_OUT_OF_RANGE."}],"responses":{"200":{"description":"Alert list (newest first) plus open-alert counts for this tenant.","content":{"application/json":{"schema":{"type":"object","required":["alerts","open_critical_count","open_high_count","total"],"properties":{"alerts":{"type":"array","items":{"$ref":"#/components/schemas/Alert"}},"open_critical_count":{"type":"integer","description":"Tenant-wide count of OPEN critical alerts (not just this page)."},"open_high_count":{"type":"integer"},"total":{"type":"integer","description":"Number of items in THIS page."}}}}}},"400":{"description":"Filter validation failure. Codes: UNSUPPORTED_FILTER, INVALID_FILTER_VALUE, LIMIT_OUT_OF_RANGE, OFFSET_OUT_OF_RANGE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Listing failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/alerts/{alertId}/acknowledge":{"post":{"tags":["Alerts"],"summary":"Acknowledge an alert","description":"Marks the alert acknowledged and records who, when, and what action was taken. 'notes' is the canonical free-text field; 'resolution_notes' (the storage column name) is a documented alias — when both are sent, notes wins.","parameters":[{"name":"alertId","in":"path","required":true,"schema":{"type":"string"},"description":"Alert ID (alrt_...)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["acknowledged_by","action_taken"],"properties":{"acknowledged_by":{"type":"string"},"action_taken":{"type":"string","enum":["revoked_access","confirmed_authorized","escalated_to_security","filed_incident_report","no_action_required"]},"notes":{"type":"string","description":"Canonical free-text field."},"resolution_notes":{"type":"string","description":"Alias for notes (backward compatibility)."}}}}}},"responses":{"200":{"description":"Alert acknowledged.","content":{"application/json":{"schema":{"type":"object","required":["alert_id","status","acknowledged_by","acknowledged_at","action_taken","notes"],"properties":{"alert_id":{"type":"string"},"status":{"type":"string","enum":["acknowledged"]},"acknowledged_by":{"type":"string"},"acknowledged_at":{"type":"string","format":"date-time"},"action_taken":{"type":"string","enum":["revoked_access","confirmed_authorized","escalated_to_security","filed_incident_report","no_action_required"]},"notes":{"type":"string","nullable":true}}}}}},"400":{"description":"Validation failure. Codes: MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Alert not found for this tenant (ALERT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Acknowledge failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/anchor/status":{"get":{"tags":["Anchoring"],"summary":"Anchoring status (honest: what actually ran)","description":"Reports recent anchor runs and this tenant's pending anchor backlog. Honesty notes baked into the response: anchoring_mode is 'mock' only when SOLANA_MOCK=true (never inferred); the network is Solana DEVNET; anchor runs happen only when the secret-protected external trigger is called — recent_anchors is the record of what actually ran. pending_batches is scoped to the CALLING API key (your own pending counts only, never platform-wide volume). recent_anchors carries only publicly-verifiable, already-on-chain fields (batch_id, anchor_date, merkle_root, ethereum_tx_hash, ethereum_block, status, mock, anchored_at) — the per-batch cross-tenant record count is deliberately NOT returned.","responses":{"200":{"description":"Current anchoring state.","content":{"application/json":{"schema":{"type":"object","required":["recent_anchors","pending_batches","anchoring_mode","network","anchor_frequency"],"properties":{"recent_anchors":{"type":"array","items":{"type":"object","description":"Publicly-verifiable on-chain anchor row — explicit column list: batch_id, anchor_date, merkle_root, ethereum_tx_hash, ethereum_block, status, mock, anchored_at. No cross-tenant per-batch record count."},"description":"Up to 10 most recent anchor runs."},"pending_batches":{"type":"array","items":{"type":"object","properties":{"count":{"type":"string","description":"Row count (returned as a string by the database driver)."},"date":{"type":"string"}}},"description":"This tenant's own pending backlog: per-day counts of the caller's transactions still awaiting anchoring (scoped to your API key)."},"anchoring_mode":{"type":"string","enum":["mock","real"],"description":"'mock' only via SOLANA_MOCK=true."},"network":{"type":"string","enum":["devnet (mock)","devnet"],"description":"Solana devnet in both modes."},"anchor_frequency":{"type":"string","description":"Honest statement: daily via an external scheduler (GitHub Actions cron) calling the secret-protected trigger; recent_anchors is the record of actual runs."}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Status lookup failed. NOTE: this legacy error body has 'error' and 'message' but no 'code'.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["internal_error"]},"message":{"type":"string"}}}}}}}}},"/v1/anchor/proof/{receiptId}":{"get":{"tags":["Anchoring"],"summary":"Merkle proof for one receipt","description":"Returns the Merkle inclusion proof for an ANCHORED receipt (leaf hash, proof steps, root, and the Solana devnet transaction that carries the root), or an honest proof_available: false body when the receipt is not yet anchored. Tenant-scoped lookup.","parameters":[{"name":"receiptId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Proof, or not-yet-anchored notice — discriminate on proof_available.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","title":"NotYetAnchored","required":["receipt_id","anchor_status","message","proof_available"],"properties":{"receipt_id":{"type":"string"},"anchor_status":{"type":"string","description":"e.g. 'pending_24h_anchor'."},"message":{"type":"string"},"proof_available":{"type":"boolean","enum":[false]}}},{"type":"object","title":"MerkleProof","required":["receipt_id","proof_available","anchor_batch","merkle_root","merkle_proof","leaf_hash"],"properties":{"receipt_id":{"type":"string"},"proof_available":{"type":"boolean","enum":[true]},"anchor_batch":{"type":"string"},"merkle_root":{"type":"string"},"merkle_proof":{"type":"array","items":{"type":"object","description":"One proof step: sibling hash + position."}},"leaf_hash":{"type":"string","description":"This receipt's record hash (hex, sha256: prefix stripped)."},"solana_tx_hash":{"type":"string","nullable":true,"description":"Solana devnet transaction carrying the Merkle root (null for mock anchors)."},"anchor_mock":{"type":"boolean","nullable":true,"description":"True when the anchor row was recorded in mock mode (nothing was written on-chain)."},"solana_explorer":{"type":"string","nullable":true,"description":"Explorer link (devnet cluster); null for mock anchors."},"anchored_at":{"type":"string","nullable":true},"verification_instructions":{"type":"string"}}}]}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Transaction not found for this tenant. NOTE: this legacy error body has 'error' and 'message' but no 'code'.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["not_found"]},"message":{"type":"string"}}}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Proof generation failed. NOTE: this legacy error body has 'error' and 'message' but no 'code'.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["internal_error"]},"message":{"type":"string"}}}}}}}}},"/v1/anchor/run":{"post":{"tags":["Anchoring"],"summary":"Trigger an anchor run (external scheduler only)","description":"NOT a tenant endpoint: mounted outside tenant auth and gated by the x-anchor-secret header (timing-safe comparison against ANCHOR_TRIGGER_SECRET). Fail-closed: when the secret is not configured on the server, the endpoint answers 503 ANCHOR_TRIGGER_DISABLED; a missing or wrong secret answers 401 with deliberately no detail. Anchors the given (or by default, yesterday's UTC) batch: builds the Merkle tree of that day's record hashes and writes the root to Solana DEVNET in a memo transaction — in mock mode (SOLANA_MOCK=true) an anchor row is recorded but nothing is written on-chain, and the response says so via mock: true / anchored: false.","security":[{"AnchorSecret":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Optional UTC day to anchor, 'YYYY-MM-DD'. Defaults to yesterday (UTC). Malformed or impossible calendar dates answer 400 INVALID_FIELD_VALUE."}}}}},"description":"Optional; may be omitted entirely."},"responses":{"200":{"description":"Run completed (or nothing to anchor) — discriminate on the anchored field.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","title":"NothingToAnchor","required":["message","anchored"],"properties":{"message":{"type":"string","example":"No transactions to anchor for this date"},"anchored":{"type":"boolean","enum":[false]}}},{"type":"object","title":"AnchorRunResult","required":["anchored","batch_id","date","merkle_root","record_count","mock"],"properties":{"anchored":{"type":"boolean","description":"True only for a REAL (non-mock) on-chain anchor."},"batch_id":{"type":"string"},"date":{"type":"string"},"merkle_root":{"type":"string"},"solana_tx":{"type":"string","nullable":true,"description":"Devnet transaction signature (labeled placeholder in mock mode)."},"record_count":{"type":"integer"},"mock":{"type":"boolean"},"solana_explorer":{"type":"string","nullable":true,"description":"Explorer link (devnet cluster); null in mock mode."}}}]}}}},"400":{"description":"Malformed or impossible date (INVALID_FIELD_VALUE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or wrong x-anchor-secret. Deliberately terse: { error: 'unauthorized', code: 'UNAUTHORIZED' } with no message.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"code":{"type":"string","enum":["UNAUTHORIZED"]}}}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Anchor run failed; details are kept in server logs only. NOTE: this legacy error body has 'error' and 'message' but no 'code'.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["internal_error"]},"message":{"type":"string"}}}}}},"503":{"description":"ANCHOR_TRIGGER_SECRET is not configured on the server; the trigger is disabled, fail-closed (ANCHOR_TRIGGER_DISABLED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/me":{"get":{"tags":["Tenant"],"summary":"Who am I (tenant introspection)","description":"Returns this API key's identity and live usage counts. Every field is existing production data (api_keys row + tenant-scoped counts) — no invented plan/quota fields. transactions_today uses the UTC calendar day. Strictly tenant-scoped: never reveals other tenants' existence or volumes.","responses":{"200":{"description":"Tenant profile.","content":{"application/json":{"schema":{"type":"object","required":["tenant","key_id","partner_name","status","rate_limit_rpm","created_at","usage"],"properties":{"tenant":{"type":"string","description":"Display label: display_name when set, otherwise partner_name."},"key_id":{"type":"string","description":"Public key identifier (not the secret)."},"partner_name":{"type":"string"},"status":{"type":"string","example":"active"},"rate_limit_rpm":{"type":"integer","nullable":true},"created_at":{"type":"string","format":"date-time"},"usage":{"type":"object","required":["transactions_total","transactions_today","alerts_open"],"properties":{"transactions_total":{"type":"integer"},"transactions_today":{"type":"integer","description":"UTC calendar day."},"alerts_open":{"type":"integer"}}}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"API key record vanished between auth-cache hit and lookup (TENANT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Profile lookup failed (ME_LOOKUP_FAILED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys":{"get":{"tags":["API Keys"],"summary":"List this tenant's API keys","description":"Self-service visibility only: returns key_id, status, and created_at for keys belonging to the authenticated tenant — NEVER the raw key or key_hash. Today this system has exactly one api_keys row per tenant (the row you authenticated with), so this list always has exactly one entry; the endpoint is written generically so it keeps working unchanged if that ever becomes many-to-one.","responses":{"200":{"description":"The tenant's own keys.","content":{"application/json":{"schema":{"type":"object","required":["keys","total"],"properties":{"keys":{"type":"array","items":{"type":"object","required":["key_id","status","created_at"],"properties":{"key_id":{"type":"string","description":"Public key identifier (not the secret)."},"status":{"type":"string","example":"active"},"created_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Failed to list keys (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys/{keyId}/revoke":{"post":{"tags":["API Keys"],"summary":"Revoke an API key","description":"Sets the key's status to 'revoked' and immediately evicts it from THIS instance's in-memory auth cache (src/middleware/auth.js evictApiKeyHash), so authentication with the revoked key fails on this instance right away instead of waiting out the cache's TTL (<=10s). Honest multi-instance caveat: eviction only clears the instance that handled this call — this deploy runs a single instance today, so that is the whole picture, but a future multi-instance deployment would need a shared invalidation channel for other instances to drop it before their own TTL lapses. Tenant-scoped: a key_id belonging to another tenant answers the same 404 as a key_id that does not exist at all (anti-enumeration) — in practice, because this system has one api_keys row per tenant, the only key_id an authenticated caller can ever revoke through this endpoint is the one they authenticated with.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"},"description":"Public key identifier (api_keys.key_id), e.g. axm_key_..."}],"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"type":"object","required":["key_id","status","revoked_at","effective_immediately"],"properties":{"key_id":{"type":"string"},"status":{"type":"string","enum":["revoked"]},"revoked_at":{"type":"string","format":"date-time"},"effective_immediately":{"type":"boolean","enum":[true],"description":"True for THIS instance (see the multi-instance caveat above)."}}}}}},"400":{"description":"Key already revoked (KEY_ALREADY_REVOKED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Key not found for this tenant (KEY_NOT_FOUND) — also returned for a key_id belonging to another tenant (anti-enumeration).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Revocation failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhooks":{"put":{"tags":["Webhooks"],"summary":"Register or replace this tenant's webhook (rotates the secret)","description":"One webhook per tenant. The server generates a new HMAC secret and returns it EXACTLY ONCE in this response — it is not retrievable later (GET only reports secret_set: true). Calling PUT again rotates the secret and reactivates a deactivated webhook. URL rules: https:// only, no embedded credentials, at most 2048 characters, and the host must not be localhost/private/link-local/metadata (the host is checked against a static deny-list AND DNS-resolved, with every resolved address (IPv4, IPv6, and IPv4-mapped IPv6) screened for private/loopback/link-local/metadata ranges at registration; delivery re-screens and pins the connection to the vetted IP on every attempt, closing the DNS-rebinding/TOCTOU window). Delivery contract: on alert creation the server POSTs {event: 'alert.created', delivery_id, sent_at, alert} with headers X-Axiom-Event, X-Axiom-Delivery, and X-Axiom-Signature: sha256=<hex HMAC-SHA256 over the exact raw body bytes, keyed with your secret>. Verify over the RAW bytes before any JSON parsing. Honest durability limitation: retries (2, in-process backoff) are lost if the server restarts while pending — delivery is NOT guaranteed; after 20 consecutive failed deliveries the webhook auto-deactivates and must be re-registered.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","maxLength":2048,"description":"https:// URL to receive alert.created events."}}}}}},"responses":{"200":{"description":"Webhook registered. STORE THE SECRET NOW — it is never returned again.","content":{"application/json":{"schema":{"type":"object","required":["url","secret","active","note"],"properties":{"url":{"type":"string"},"secret":{"type":"string","description":"64-char hex (32 random bytes). Returned only in this response."},"active":{"type":"boolean","enum":[true]},"note":{"type":"string","example":"store this; not retrievable later"}}}}}},"400":{"description":"URL rejected. Codes: MISSING_URL, INVALID_URL, HTTPS_REQUIRED, URL_CREDENTIALS_FORBIDDEN, FORBIDDEN_URL_TARGET, URL_UNRESOLVABLE, URL_TOO_LONG.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Registration failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"tags":["Webhooks"],"summary":"Current webhook configuration (never returns the secret)","description":"Returns the configured URL, delivery health, and machine-readable signature-verification documentation. The secret itself is never returned (secret_set: true only).","responses":{"200":{"description":"Current configuration.","content":{"application/json":{"schema":{"type":"object","required":["url","active","secret_set","created_at","last_success_at","last_failure_at","failure_count","verification"],"properties":{"url":{"type":"string"},"active":{"type":"boolean"},"secret_set":{"type":"boolean","enum":[true]},"created_at":{"type":"string","format":"date-time"},"last_success_at":{"type":"string","format":"date-time","nullable":true},"last_failure_at":{"type":"string","format":"date-time","nullable":true},"failure_count":{"type":"integer","description":"CONSECUTIVE failed deliveries; any success resets to 0. At 20 the webhook auto-deactivates."},"verification":{"type":"object","description":"How to verify delivery signatures, including a Node example.","properties":{"signature_header":{"type":"string","example":"X-Axiom-Signature"},"event_header":{"type":"string","example":"X-Axiom-Event"},"delivery_header":{"type":"string","example":"X-Axiom-Delivery"},"algorithm":{"type":"string","description":"HMAC-SHA256 of the exact raw request body bytes, keyed with your webhook secret, hex-encoded, sent as 'sha256=<hex>'."},"verify_example_node":{"type":"string"}}}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No webhook configured for this tenant (WEBHOOK_NOT_CONFIGURED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Lookup failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Deactivate this tenant's webhook","description":"Deactivates delivery (idempotent on an already-inactive webhook). The row and secret are kept; PUT re-enables with a FRESH secret. 404 only when nothing was ever configured.","responses":{"200":{"description":"Webhook deactivated.","content":{"application/json":{"schema":{"type":"object","required":["url","active"],"properties":{"url":{"type":"string"},"active":{"type":"boolean","enum":[false]}}}}}},"401":{"description":"Missing or invalid x-axiom-key (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No webhook configured for this tenant (WEBHOOK_NOT_CONFIGURED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Deactivation failed (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/openapi.json":{"get":{"tags":["Documentation"],"summary":"This OpenAPI 3.0.3 document","description":"Public. Served with Cache-Control max-age=300.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object","description":"OpenAPI 3.0.3 document (this file)."}}}}}}},"/docs":{"get":{"tags":["Documentation"],"summary":"Human-readable API reference","description":"Public. Fully self-contained HTML page (no external scripts, styles, or fonts) that fetches /openapi.json and renders it client-side.","security":[],"responses":{"200":{"description":"HTML reference page.","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-axiom-key","description":"Tenant API key. All endpoints require it except the public ones (health, well-known key, verify, docs) and the anchor trigger (which uses AnchorSecret instead)."},"AnchorSecret":{"type":"apiKey","in":"header","name":"x-anchor-secret","description":"Shared secret for the external anchor scheduler (POST /v1/anchor/run only). Compared timing-safe against ANCHOR_TRIGGER_SECRET; when that variable is unset the endpoint is disabled (503, fail-closed)."}},"schemas":{"ErrorEnvelope":{"type":"object","description":"Standard error body for 4xx/5xx responses. 'error' is the coarse category (backward compatible); 'code' is a stable UPPERCASE machine code from the append-only registry in src/middleware/errors.js — match on code, never parse message. 'field' names the offending body field or query parameter when one is at fault. A few legacy anchor error responses omit 'code' (noted on those endpoints).","required":["error","message"],"properties":{"error":{"type":"string","enum":["invalid_request","unauthorized","not_found","rate_limited","internal_error","anchor_trigger_disabled","service_unavailable"],"description":"Coarse category."},"code":{"type":"string","enum":["INVALID_JSON","PAYLOAD_TOO_LARGE","MISSING_REQUIRED_FIELD","INVALID_FIELD_VALUE","INVALID_TIMESTAMP","EXPIRES_AT_IN_PAST","UNSUPPORTED_FILTER","INVALID_FILTER_VALUE","LIMIT_OUT_OF_RANGE","OFFSET_OUT_OF_RANGE","ROUTE_NOT_FOUND","RECEIPT_NOT_FOUND","GRANT_NOT_FOUND","ALERT_NOT_FOUND","TRANSACTION_NOT_FOUND","KEY_NOT_FOUND","GRANT_ALREADY_REVOKED","KEY_ALREADY_REVOKED","UNAUTHORIZED","RATE_LIMITED","INTERNAL_ERROR","SERVICE_UNAVAILABLE","ANCHOR_TRIGGER_DISABLED","SIGNUP_UNAVAILABLE","SIGNUP_TOKEN_INVALID","SIGNUP_TOKEN_EXPIRED","SIGNUP_ALREADY_VERIFIED","IDEMPOTENCY_KEY_REUSE","IDEMPOTENCY_KEY_INVALID","BATCH_TOO_LARGE","INVALID_BATCH_ITEM","BATCH_BODY_INVALID","BATCH_EMPTY","BATCH_ITEM_NOT_OBJECT","BATCH_HEADER_IDEMPOTENCY_UNSUPPORTED","BODY_NOT_OBJECT","DATA_ACCESSED_EMPTY","DATA_ACCESSED_ITEM_INVALID","CLASSIFICATION_INVALID","PURPOSE_INVALID","AUTHORIZATION_METHOD_INVALID","LOG_FAILED","TENANT_NOT_FOUND","ME_LOOKUP_FAILED","MISSING_URL","INVALID_URL","HTTPS_REQUIRED","URL_CREDENTIALS_FORBIDDEN","FORBIDDEN_URL_TARGET","FORBIDDEN_URL_PORT","URL_TOO_LONG","URL_UNRESOLVABLE","WEBHOOK_NOT_CONFIGURED","METADATA_TOO_DEEP"],"description":"Stable machine code (append-only registry)."},"message":{"type":"string","description":"Human-readable detail. May be reworded — never parse it."},"field":{"type":"string","description":"Offending body field or query parameter (present only when one is at fault)."}}},"DataAccessedItem":{"type":"object","required":["resource_id","resource_type","classification"],"properties":{"resource_id":{"type":"string"},"resource_type":{"type":"string","description":"Matched against grant data_categories by the compliance engine."},"classification":{"type":"string","enum":["PHI","PII","CONFIDENTIAL","INTERNAL","PUBLIC"],"description":"Data classification, enum-validated by POST /v1/log and /v1/log/batch (400 CLASSIFICATION_INVALID otherwise). The compliance engine ranks these PUBLIC < INTERNAL < CONFIDENTIAL < PII < PHI against the grant's classification_ceiling.","example":"PHI"}},"description":"One accessed resource. Additional properties are stored as given."},"LogRequest":{"type":"object","required":["model_id","data_accessed","authorized_by","authorization_method","purpose","session_id","expires_at"],"properties":{"model_id":{"type":"string","description":"The AI system that accessed the data."},"model_version":{"type":"string","nullable":true},"data_accessed":{"type":"array","items":{"$ref":"#/components/schemas/DataAccessedItem"},"minItems":1},"authorized_by":{"type":"string"},"authorization_method":{"type":"string","enum":["role_based","explicit_user_consent","system_service_account","emergency_override","delegated"]},"purpose":{"type":"string","enum":["diagnostic_assistance","fraud_detection","content_moderation","recommendation","data_analysis","autonomous_action","research","other"]},"session_id":{"type":"string"},"expires_at":{"type":"string","format":"date-time","description":"Authorization expiry; must parse and be in the future (400 INVALID_TIMESTAMP / EXPIRES_AT_IN_PAST otherwise)."},"patient_consent_id":{"type":"string","nullable":true},"metadata":{"type":"object","description":"Free-form; stored and signed as part of the record."}}},"LogRequestBatchItem":{"allOf":[{"$ref":"#/components/schemas/LogRequest"},{"type":"object","properties":{"idempotency_key":{"type":"string","minLength":1,"maxLength":255,"description":"Optional per-item dedupe key — same semantics and storage as the single-endpoint Idempotency-Key header (the two endpoints dedupe against each other). NOT material to body comparison."}}}],"description":"A /v1/log body plus an optional per-item idempotency_key."},"BatchResultItem":{"type":"object","required":["index","status"],"description":"Per-item outcome, in input order. Success items (status 201, or 200 for an idempotent replay) carry receipt; failed items carry the standard error envelope fields inline (error, code, message, field?).","properties":{"index":{"type":"integer","description":"Position in the submitted batch."},"status":{"type":"integer","enum":[200,201,400,409,500],"description":"201 created; 200 idempotent replay; 400/409/500 per-item failure."},"receipt":{"$ref":"#/components/schemas/Receipt"},"idempotent_replay":{"type":"boolean","description":"Present (true) only when this item replayed a previously stored response."},"error":{"type":"string","description":"Failure items only: coarse category."},"code":{"type":"string","description":"Failure items only: machine code (see ErrorEnvelope)."},"message":{"type":"string","description":"Failure items only."},"field":{"type":"string","description":"Failure items only, when a field is at fault."}}},"Receipt":{"type":"object","description":"v4 signed receipt. The signature is RSA-PSS/SHA-256 over record_hash; anyone can verify it with the public key at /.well-known/axiom-public-key.pem — the signature math runs locally once you have the key (the key is served from this API host; pin the PEM out-of-band if your threat model includes the operator). Anchoring state is reported honestly: receipts start as pending_24h_anchor and are only marked anchored after an actual Solana devnet anchor run.","required":["receipt_id","record_hash","signature","timestamp","anchor_batch","anchor_status","expires_at","compliance_check_status","verification_url"],"properties":{"receipt_id":{"type":"string","description":"Receipt identifier: 'axm_rec_' + 26 uppercase hex characters.","example":"axm_rec_9F2C41D08A7B3E65C1D4F0A2B8"},"record_hash":{"type":"string","description":"'sha256:<hex>' over a canonical serialization of the signed record. The stored record_hash is what the signature commits to and is the authoritative verification input."},"signature":{"type":"string","description":"'axiom_sig_v1:<base64>' — RSA-PSS/SHA-256 signature over record_hash (v4 receipt model)."},"timestamp":{"type":"string","format":"date-time","description":"Server time the record was signed."},"anchor_batch":{"type":"string","description":"Daily batch label: 'batch_YYYYMMDD' (UTC).","example":"batch_20260705"},"anchor_status":{"type":"string","description":"Always 'pending_24h_anchor' at creation. Becomes 'anchored' only after a real anchor run includes this batch (see POST /v1/anchor/run and GET /v1/anchor/status)."},"expires_at":{"type":"string","format":"date-time","description":"Echo of the submitted authorization expiry."},"compliance_check_status":{"type":"string","enum":["passed","failed","error"],"description":"Result of the inline compliance check against this tenant's active grants. 'error' means the engine could not evaluate the access — it is deliberately distinct from 'passed' (fail-closed reporting)."},"verification_url":{"type":"string","description":"Public verification URL for this receipt (GET /v1/verify/{receiptId}, no auth needed)."},"compliance_alerts":{"type":"array","items":{"$ref":"#/components/schemas/ComplianceViolation"},"description":"Present only when compliance_check_status is 'failed' — the same violation objects are also persisted as alerts (GET /v1/alerts). Omitted on 'passed' and 'error'."}}},"GrantEventReceipt":{"type":"object","description":"Signed receipt recording the grant lifecycle event itself in the transactions ledger. Same v4 signature model as a log receipt; no inline compliance fields.","required":["receipt_id","record_hash","signature","timestamp","anchor_batch","anchor_status","verification_url"],"properties":{"receipt_id":{"type":"string"},"record_hash":{"type":"string"},"signature":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"anchor_batch":{"type":"string"},"anchor_status":{"type":"string"},"verification_url":{"type":"string"}}},"ComplianceViolation":{"type":"object","required":["type","severity","description","receipt_id"],"properties":{"type":{"type":"string","enum":["unauthorized_data_access","denied_field_accessed","field_not_in_allowlist","access_after_expiry","classification_ceiling_exceeded","volume_exceeded"]},"severity":{"type":"string","enum":["critical","high","medium","low"],"description":"The engine currently emits 'critical' and 'high' only."},"description":{"type":"string"},"grant_id":{"type":"string","nullable":true,"description":"The grant the violation is attributed to, or null when not attributable to a single grant."},"receipt_id":{"type":"string"}}},"TransactionSummary":{"type":"object","description":"One row of GET /v1/transactions.","required":["receipt_id","record_hash","timestamp","model_id","authorized_by","purpose","anchor_status","expires_at"],"properties":{"receipt_id":{"type":"string"},"record_hash":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"model_id":{"type":"string"},"authorized_by":{"type":"string"},"purpose":{"type":"string"},"anchor_status":{"type":"string"},"expires_at":{"type":"string"}}},"GrantRow":{"type":"object","description":"Full stored access_grants row as returned by GET /v1/access/grants[/{grantId}] — includes grant_id, ai_system_id, data_categories, classification_ceiling, authorized_by, authorization_basis, purpose, expires_at, allowed_fields, denied_fields, grant_hash, signature, status, created_at, and (when revoked) revoked_at / revoked_by / revocation_reason. Exact column set is the database row.","properties":{"grant_id":{"type":"string"},"ai_system_id":{"type":"string"},"data_categories":{"type":"array","items":{"type":"string"}},"classification_ceiling":{"type":"string"},"authorized_by":{"type":"string"},"authorization_basis":{"type":"string","nullable":true},"purpose":{"type":"string"},"expires_at":{"type":"string"},"allowed_fields":{"type":"array","items":{"type":"string"}},"denied_fields":{"type":"array","items":{"type":"string"}},"grant_hash":{"type":"string"},"signature":{"type":"string"},"status":{"type":"string","description":"'active' or 'revoked'."},"created_at":{"type":"string","format":"date-time"},"revoked_at":{"type":"string","format":"date-time","nullable":true},"revoked_by":{"type":"string","nullable":true},"revocation_reason":{"type":"string","nullable":true}}},"Alert":{"type":"object","description":"Stored alert row plus the canonical 'notes' alias for resolution_notes (additive). Rows are returned as stored (SELECT *), so internal bookkeeping columns may also appear.","properties":{"alert_id":{"type":"string","example":"alrt_9F2C41D08A7B3E65C1D4F0A2B8"},"type":{"type":"string","enum":["unauthorized_data_access","denied_field_accessed","field_not_in_allowlist","access_after_expiry","classification_ceiling_exceeded","volume_exceeded","compliance_check_error"]},"severity":{"type":"string","enum":["critical","high","medium","low"]},"description":{"type":"string"},"triggered_by_receipt_id":{"type":"string","nullable":true},"status":{"type":"string","enum":["open","acknowledged"]},"triggered_at":{"type":"string","format":"date-time"},"acknowledged_by":{"type":"string","nullable":true},"acknowledged_at":{"type":"string","format":"date-time","nullable":true},"resolution_notes":{"type":"string","nullable":true,"description":"Storage column name (legacy)."},"notes":{"type":"string","nullable":true,"description":"Canonical alias for resolution_notes."}}}}}}