Reference
Error handling
One error shape across every operation, with stable codes, a correlation identifier for support, and a deliberate disclosure policy about what a failure is allowed to reveal.
#The envelope
Every failure returns the same structure, so a client parses errors once:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request failed validation.",
"correlationId": "01J8Z5K2QK7N9V3B4C5D6E7F8G",
"issues": [
{ "field": "quantity.value", "code": "OUT_OF_RANGE", "message": "Quantity exceeds the supported maximum." }
]
}
}
X-Correlation-Id. Quote it in support requests.#Disclosure policy
No stack trace, SQL fragment, driver detail, internal identifier, or upstream failure body is ever returned. Errors that would otherwise confirm the existence of another tenant's resource return a non-disclosure response instead. Grant failures deliberately collapse into one indistinguishable response so responses cannot be used to enumerate valid grants.
#Common codes
| Code | Status | Meaning | Retryable |
|---|---|---|---|
VALIDATION_FAILED | 400 | Structural or mechanical validation failed. Nothing persisted. | Only after changing the request |
UNAUTHENTICATED | 401 | Missing or invalid credential. | After obtaining a valid credential |
FORBIDDEN | 403 | Authenticated, but not authorized for this resource or action. | No |
NOT_FOUND | 404 | No such resource, or non-disclosure of one outside your tenant. | No |
IDEMPOTENCY_CONFLICT | 409 | Key reused with a materially different request body. | No — use a new key |
IDEMPOTENCY_KEY_EXPIRED | 410 | Key aged into a permanent tombstone. It can never be replayed. | No — use a new key |
REVIEW_UNAVAILABLE | 410 | Grant or session unknown, expired, consumed, or invalidated. | No — a new grant is required |
RATE_LIMITED | 429 | Too many requests. Honour Retry-After. | Yes, after the interval |
DEPENDENCY_UNAVAILABLE | 503 | A required dependency is unavailable. No partial effect occurred. | Yes, with backoff |
DEPENDENCY_TIMEOUT | 504 | A dependency exceeded its deadline. | Yes, with the same idempotency key |
#Retrying safely
Reuse the same idempotency key when retrying a draft creation after a timeout or transport failure. That is precisely the case idempotency exists for: if the original request committed, the retry returns the stored response with Idempotency-Replayed: true and no second draft is created. Generating a fresh key on retry defeats the protection and can create a duplicate.
Do not retry 400, 403, 409, or 410 without changing something — they are deterministic and will fail identically. Retry 429, 503, and 504 with exponential backoff and jitter.
#Edge-generated failures
The Nginx edge can produce failures before a request reaches the application — connection refusal, upstream timeout, or an oversized body. It returns canonical no-store JSON envelopes for these rather than an HTML error page, so a client parses them the same way. A dedicated verification script proves exact backend 400, 502, 503, and 504 pass-through alongside edge-generated 502 and 504 responses.