API errors (RFC 9457)

Proofroom returns application/problem+json for agent-facing API failures on evidence write paths (POST /api/events, POST /api/webhook, and MCP tool errors). Agents should read error_code and remediation, not guess from a bare 401.

Problem shape

Field Meaning
type URI to this page with a fragment, e.g. https://proofroom.ai/docs/errors#room_unclaimed
title Short human title
status HTTP status
detail What happened
instance Request path
error_code Machine-readable cause
remediation What to do next
trace_id Correlation id for operator support
claim_url Present for room_unclaimed when a claim link exists
retryable / retry_class Whether to retry (backoff) or escalate (human_action)

Error codes

Code Status Retry Notes
missing_api_key 401 Human Supply Authorization: Bearer prf_live_…
invalid_api_key 401 Human Unknown key
revoked_api_key 401 Human Issue a new key; do not retry
key_room_mismatch 403 Human Key bound to a different agent
room_unclaimed 403 Human Claim incomplete / draft archived — follow claim_url
room_retired 403 Human Room revoked
scope_violation 403 Human Event outside declared scope
room_not_found 404 Human Wrong slug/id for this org
receipt_not_found 404 Human Unknown receipt slug (GET /api/receipt/{slug})
private_room 403 Human Receipt/room is private — need share or public listing
validation_failed 422 None Fix the body
rate_limited 429 Backoff Honour Retry-After (120/min per key on /api/events)
idempotency_conflict 409 Human Same Idempotency-Key with a different body — use a new key
invalid_json 400 None Send valid JSON
internal_error 500 Backoff Transient; then escalate

Idempotency (§4)

POST /api/events and POST /api/webhook accept Idempotency-Key (header preferred; body idempotency_key also works). Same key + same body within ≥24h returns the original event (200, idempotent_replay: true). Same key + different body → idempotency_conflict.

Corrections (§4)

Use event_type: "correction_recorded" with corrects_event_id (prior event UUID on the same room). Wrong rows stay; hashes are never rewritten. Public verify: GET /api/verify/{slug}.

Room health on success

Successful record_evidence_event / get_trust_status responses include room_health:

  • state — provisional health state (pending_claim, active, stale_warning, degraded, …)
  • days_until_stale / days_until_degraded
  • warnings[] — e.g. approaching decay, or claim incomplete

Use heartbeat as event_type for a cheap liveness ping (chained, not receipted as substantive work).

Example: unclaimed / archived draft

{
  "type": "https://proofroom.ai/docs/errors#room_unclaimed",
  "title": "Room pending claim review",
  "status": 403,
  "detail": "This room was never activated — claim review is incomplete and the draft has been archived.",
  "instance": "/api/events",
  "error_code": "room_unclaimed",
  "remediation": "Complete claim review at claim_url before evidence can be recorded.",
  "claim_url": "https://proofroom.ai/claim/…",
  "trace_id": "prf_…",
  "retryable": false,
  "retry_class": "human_action"
}