Errors & conventions
Assert these in every integration. The platform's promise is that refusals are self-diagnosing — the error body alone should tell a misconfigured client what to fix.
Universal request conventions
| Convention | Contract |
|---|---|
| Authentication | Authorization: Bearer <api-key-or-oauth-token> on every call. x-api-key-shaped headers also pass the edge. |
| Idempotency | Idempotency-Key: <uuid> on mutating POSTs. Required on publish-dpp and oauth/token; accepted & recommended elsewhere. |
| Content type | application/json unless the endpoint says multipart. |
| Errors | Typed envelope { error: { code, message, details? } }. |
| Correlation | Optional X-Correlation-Id flows route → use case → audit log. |
The error envelope
JSON
{
"error": {
"code": "API_SCOPE_DENIED",
"message": "Missing required scope for this operation.",
"details": { "required": "products:create", "available": ["products:view"] }
}
}Error catalogue
The codes an integrator must branch on. Two are documented deviations — asserted as-is until the underlying contract changes.
| Status | Code | Trigger |
|---|---|---|
| 401 | — | Bad/garbage or missing credential; also role-level permission denials. |
| 403 | API_SCOPE_DENIED | Good credential, missing scope. Carries { required, available }. |
| 403 | FORBIDDEN | Out-of-scope on publish-dpp — different auth wrapper. Deviation. |
| 403 | SANDBOX_EXPIRED | Sandbox tenant past expiry (both conditions required). |
| 403 | GUARD_REJECTED | Illegal passport lifecycle transition (e.g. EDIT a published passport). |
| 400 | VALIDATION_ERROR | Field validation — names the field/bound (e.g. 21-char serial, description < 10). |
| 422 | EXTENSIONS_REQUIRED | Missing extensions on product create. |
| 422 | SCHEMA_NOT_FOUND | Category has no effective schema (see the category-hint gotcha). |
| 400 | INVALID_GTIN / INVALID_SERIAL | Bad check digit / over-long serial at the resolver. |
| 404 | PRODUCT_NOT_FOUND | Unknown-but-valid GTIN at the resolver — names gtin/serial/host. |
| 404 | — | Draft/nonexistent public passport; cross-tenant fetch. Authz runs before existence — no id enumeration. |
| 410 | SERIAL_VOIDED | Voided serialized item — returns a tombstone, zero passport content. |
| 422 | UNKNOWN_EVENT_TYPE | Webhook subscription to a non-catalogue event name. |
| 422 | PASSPORT_NOT_PUBLISHABLE | Registry broadcast attempted on a passport whose lifecycle state is not publishable. |
| 409 | DUPLICATE_GTIN | A second model-granularity product with the same GTIN in this tenant — (company_id, gtin) is unique. |
| 409 | ALREADY_REVOKED | Revoking a key that is already revoked. Note that rotate revokes the old id for you and returns a NEW data.id — revoke that one. |
| 409 | — | Conflict: re-void, same-key different-body idempotency, scheme-lock change, illegal passport command transition. |
| 503 | COMPLIANCE_EVALUATION_UNAVAILABLE | Battery only, and deliberate: the governed battery obligation pack is quarantined (ATLAS-F017), so no battery compliance verdict may be issued. This is NOT_EVALUATED — a fail-closed refusal to judge, never a compliance failure. Every other category evaluates normally; start on electronics. |
| 503 | REGISTRY_UNAVAILABLE | No external registry (CATENA-X / GS1 / EBSI) is configured — the normal sandbox answer to POST /passports/{id}/publish. |
| 503 | ANCHORING_UNAVAILABLE | Blockchain not configured in this environment (ENABLE_BLOCKCHAIN=false) — the normal sandbox answer. |
| 500 | ANCHORING_FAILED | A real anchoring fault on a configured chain. Distinct from ANCHORING_UNAVAILABLE (503): only this one means something is wrong. |
| 405 | — | Wrong verb on a route that does not support it. (Product update accepts PATCH — canonical, PRD F1 — and PUT as an alias.) |
| 302 | — | Resolver redirect to /p/{uid} (the public uid = the product id). |
| 202 | — | Async accepted (generation, imports). |
Rate limiting is enforced (PRD F10): per-tier budgets on a Redis-backed limiter. Over budget →
429 RATE_LIMITED with X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset and Retry-After. Back off on Retry-After.Idempotency semantics
- Same key + same body → the first result is replayed, with no duplicate side effects.
- Same key + different body → a typed conflict.
- Enforcement is per-route:
publish-dppandoauth/tokenrequire a key;POST /productswithout one still returns201.