Docs · Errors
Errors
Every error is an HTTP status and a JSON body with a stable code and a message. Match on the code; the message can change.
Error body
{
"error": "invalid_api_key",
"message": "The API key is invalid or does not exist. Double-check the key and try again."
}
errorStable machine-readable code; the catalogue below lists every value the API returns.
messageHuman-readable explanation; the wording may change, the code does not.
retry_afterOnly on 429 rate_limited: seconds to wait, the same value as the Retry-After header.
plan_tierOnly on 429 rate_limited: the tier name derived from the limit that applied.
limit_per_secOnly on 429 rate_limited: the per-second limit that applied.
upgrade_urlOnly on 429 rate_limited: the dashboard path for changing plan, /dashboard/plans/upgrade.
Status codes
| 400 | The request failed validation; the code names the field or rule. |
|---|---|
| 401 | No key, a malformed key, an unknown key or a revoked key. |
| 402 | The subscription is past due on a paid tier, so writes are blocked. |
| 403 | The key lacks the scope, the address is not on its IP allowlist, or the plan does not include the feature. |
| 404 | No record with that id for this tenant, or no public proof for it. |
| 409 | The write conflicts with stored state: a reused Idempotency-Key, an anchored hash, a trace that is not open. |
| 410 | The record passed its retention window and is no longer served. |
| 413 | An AI attestation's canonical JSON exceeds 1 MB. |
| 429 | rate_limited when the per-second or per-minute budget is used up; quota_exceeded when the monthly plan quota is. |
| 5xx | Something failed on the server; see Retries. |
Authentication and authorization
Validation
| Code | Status | When |
|---|---|---|
| invalid_path_parameter | 400 | A path id on an events, documents or AI attestations get, raw or verify call is not a UUID. |
| bad_request | 400 | An authenticated verify call on events, documents or AI attestations has a malformed or missing hash, or both payload and payload_hash. |
| invalid_event_type | 400 | event_type is empty or longer than 128 characters. |
| invalid_payload | 400 | The event payload is not a JSON object, or the payload sent to the public event verify could not be serialized. |
| payload_too_large | 400 | An event payload or an audit event body exceeds 64 KB. |
| payload_too_large | 413 | The canonical JSON of an AI attestation exceeds 1 MB, or a document anchor body exceeds the route's 8,462,336-byte limit. |
| empty_payload | 400 | An AI attestation's input or output is empty. |
| invalid_attestation_type | 400 | An AI attestation type is not one the backend accepts. |
| invalid_context | 400 | An AI attestation's model context is incomplete. |
| subject_too_many_keys | 400 | An AI attestation subject carries more than 20 extra keys. |
| subject_too_large | 400 | An AI attestation subject serializes to more than 8 KB. |
| invalid_document_hash | 400 | document_hash is not 64 hex characters. |
| invalid_metadata | 400 | Document metadata is not a JSON object, or audit event metadata breaks the key count, key length or value length limits. |
| metadata_too_large | 400 | Document metadata exceeds 8 KB or trace metadata exceeds 16 KB. |
| invalid_original_bytes_base64 | 400 | original_bytes_b64 is not valid base64. |
| original_bytes_empty | 400 | original_bytes_b64 decodes to zero bytes. |
| original_bytes_too_large | 400 | original_bytes_b64 decodes to more than 5 MB. |
| original_bytes_hash_mismatch | 400 | The SHA-256 of the decoded original bytes differs from document_hash. |
| invalid_label | 400 | A trace label is missing, empty or longer than 255 characters. |
| invalid_status | 400 | A trace list status filter is not open, sealing or sealed. |
| trace_empty | 400 | Sealing was requested on a trace with no events. |
| trace_invalid_status | 400 | A document anchor or AI attestation names a trace whose status accepts no new items; event ingest returns the same code with status 409. |
| invalid_idempotency_key | 400 | The Idempotency-Key header is empty, not valid UTF-8 or longer than the endpoint's limit: 128 characters on events, 255 on audit events. |
| missing_idempotency_key | 400 | POST /v1/audit/events was called without an Idempotency-Key header. |
| invalid_json | 400 | An audit event body is not valid UTF-8 JSON, or the JSON sent to the public audit verify could not be parsed. |
| duplicate_json_key | 400 | An audit event body repeats a JSON key. |
| invalid_envelope | 400 | An audit event body does not match the audit event envelope. |
| invalid_org | 400 | organization_id on an audit event is empty after trimming. |
| invalid_action | 400 | An audit event action is malformed or longer than 128 characters. |
| invalid_actor | 400 | An audit event actor is missing a required field. |
| invalid_target | 400 | An audit event target is missing a required field. |
| too_many_targets | 400 | An audit event lists more than 20 targets. |
| invalid_version | 400 | An audit event carries an unsupported envelope version. |
| invalid_occurred_at | 400 | An audit event occurred_at is not a parseable timestamp. |
| occurred_at_out_of_window | 400 | An audit event occurred_at is more than 5 years in the past or more than 24 hours in the future. |
| invalid_event | 400 | The JSON sent to POST /v1/proof/audit/{event_id}/verify is not an audit event for the id in the path. |
| invalid_organization_id | 400 | organization_id is empty on an audit event list, or empty or longer than 255 characters on audit org create. |
| unknown_org | 400 | An audit event list names an organization_id the tenant does not have. |
| invalid_timestamp | 400 | occurred_after or occurred_before on an audit event list is not a parseable timestamp. |
| invalid_cursor | 400 | An audit event list cursor is malformed. |
| invalid_name | 400 | An audit organization name is longer than 255 characters; an empty name is accepted and clears the stored one. |
| nothing_to_update | 400 | An audit organization update carries no fields. |
| invalid_days | 400 | An audit organization retention days value is less than 1; values above the plan cap are clamped, not rejected. |
| invalid_format | 400 | An audit export format is not one the backend supports. |
| unsupported_stream_type | 400 | A stream was created or tested with a type the backend does not support. |
| invalid_url | 400 | A stream destination is not a valid absolute URL. |
| not_https | 400 | A stream destination does not use https. |
| no_host | 400 | A stream destination URL has no host. |
| resolution_failed | 400 | A stream destination host did not resolve in DNS. |
| forbidden_destination | 400 | A stream destination resolves to a private, loopback, link-local or cloud metadata address. |
| no_endpoint | 400 | A stream test was requested for a stream with no endpoint. |
| too_many_streams | 400 | The plan's limit on stream destinations for the audit organization has been reached. |
| invalid_intent | 400 | A portal session intent is not one the backend recognises. |
| invalid_duration | 400 | A portal session duration is outside the accepted range; the maximum is 86400 seconds. |
| invalid_link_duration | 400 | A portal link duration is outside the accepted range; the maximum is 3600 seconds. |
| invalid_hash | 400 | A public proof verify received a hash that is not 64 hex characters. |
| missing_input | 400 | POST /v1/proof/event/{event_id}/verify received neither payload_hash nor payload. |
| invalid_domain | 400 | GET /keys/{domain} received an empty domain or one longer than 253 characters. |
Quota and billing
| Code | Status | When |
|---|---|---|
| quota_exceeded | 429 | The monthly plan quota for the resource (events, documents, AI attestations, verifications, traces or audit events) is used up; message "Plan quota exceeded. Upgrade at /dashboard/plans/upgrade". |
| feature_not_available | 403 | The plan's monthly limit for the resource is 0, so the feature is not included; message "This feature is not included in your plan. Upgrade at /dashboard/plans/upgrade". |
| payment_required | 402 | The subscription is past due on a paid tier, so writes are blocked; message "Subscription payment is past due. Update your payment method at /dashboard/billing". |
| too_many_orgs | 403 | The plan's limit on audit organizations has been reached, on create or on ingest for a new organization. |
| quota_check_failed | 500 | The plan lookup or the Redis quota counter failed; the request is rejected rather than allowed. |
Not found and expired
| Code | Status | When |
|---|---|---|
| event_not_found | 404 | No event with that id belongs to the tenant, or no public event or audit proof exists for the id. |
| document_not_found | 404 | No anchored document with that event id belongs to the tenant. |
| attestation_not_found | 404 | No AI attestation with that id belongs to the tenant. |
| ai_attestation_not_found | 404 | No public AI proof exists for that attestation id. |
| trace_not_found | 404 | No trace with that id belongs to the tenant, or no public trace proof exists for the id. |
| proof_not_found | 404 | No document anchor exists for the event id given to the public document proof. |
| not_found | 404 | An audit organization, audit event, export, stream or portal record does not exist for the tenant. |
| org_not_found | 404 | The organization named in an audit event does not exist for the tenant. |
| domain_not_found | 404 | No organization is registered with the domain given to GET /keys/{domain}. |
| domain_not_verified | 404 | The domain is registered but has not completed verification. |
| keys_not_yet_provisioned | 404 | The domain is verified but the tenant has not signed anything yet, so no key exists. |
| retention_expired | 410 | The event, document or AI attestation has passed its retention window and is no longer served. |
Conflict
| Code | Status | When |
|---|---|---|
| idempotency_key_reuse_mismatch | 409 | The Idempotency-Key was already used with a different request. |
| document_already_anchored | 409 | A document with the same hash is already anchored for the tenant. |
| duplicate_trace | 409 | A trace with the same label already exists. |
| trace_not_open | 409 | A seal, delete, document anchor or AI attestation targets a trace that is not open. |
| trace_sealed | 409 | Event ingest targets a trace that is already sealed. |
| trace_sealing | 409 | Event ingest targets a trace that is being sealed. |
| trace_has_events | 409 | Delete was requested on a trace that contains events. |
| trace_not_sealed | 409 | A proof bundle, proof PDF or public trace proof was requested for a trace that is not sealed. |
| org_exists | 409 | An audit organization with the same identifier already exists. |
| org_archived | 409 | The audit organization is archived and accepts no new events, streams, exports or portal sessions. |
| org_not_deletable | 409 | The audit organization still has signed history, data retention has not purged, or events accepted in the last 24 hours. |
Rate limit
| Code | Status | When |
|---|---|---|
| rate_limited | 429 | The tenant exceeded its per-second or per-minute request budget; Retry-After says how long to wait. |
Server
| Code | Status | When |
|---|---|---|
| db_error | 500 | A database query failed. |
| db_query_failed | 500 | The lookup behind GET /v1/document/{event_id}/original or GET /v1/ai/attestations/{attestation_id}/raw failed. |
| r2_fetch_failed | 500 | The stored original document or raw attestation bytes could not be fetched from object storage. |
| internal_error | 500 | Document anchoring hit an unexpected failure. |
| encode_failed | 500 | An event, audit event or seal envelope could not be encoded for the write queue. |
| ingest_failed | 500 | Publishing to the write queue failed; any idempotency reservation is released so a retry can succeed. |
| serialization_failed | 500 | A response or queue message could not be serialized. |
| serialization_error | 500 | A row read in GET /keys/{domain} could not be decoded. |
| hash_error | 500 | Hashing an AI attestation failed. |
| hash_decode_failed | 500 | A validated hex hash could not be decoded to bytes. |
| trace_create_failed | 500 | Creating a trace failed. |
| seal_publish_failed | 500 | Publishing the seal job failed. |
| seal_event_missing | 500 | A sealed trace has no seal event to build the proof from. |
| pdf_generation_failed | 500 | Rendering a trace proof PDF failed. |
| api_key_lookup_failed | 500 | The API key could not be looked up in the database. |
| token_sign_failed | 500 | A portal JWT could not be signed. |
| key_unavailable | 500 | The tenant's signing key could not be loaded for a public audit proof. |
Retries
429 rate_limited | Wait Retry-After seconds, then send the same request: 1 for the per-second bucket, the seconds left in the minute for the minute bucket. |
|---|---|
429 quota_exceeded | No Retry-After. The same request fails until the quota changes; the message names the upgrade path. |
202 processing | A retry arrived while the first request was still running. Wait, then retry with the same Idempotency-Key. |
| 5xx | Retry with backoff and the same Idempotency-Key. ingest_failed releases the reservation so the retry is processed; a completed request replays its stored response for 24 hours. |
| Other 4xx | Do not retry unchanged. Fix the request; 409 idempotency_key_reuse_mismatch means the key was already used with a different body. |