Core principles
- Uniform envelope. Every error response uses the structure described below. No endpoint deviates.
- HTTP status is informational. Use
error.codefor coarse routing (< 500vs>= 500). It is not granular enough for business logic. error.statusis the contract. This is the stable, machine-readable identifier you branch on. It does not change between releases.error.messageis human-facing. Wording may change. Never string-matcherror.messagein code.error.details[]carries machine context. When you need to react programmatically to a specific condition, readdetails[].reasonanddetails[].metadata— never parsemessage.
The envelope
erroris always present.error.code,error.status,error.message, anderror.detailsare always present.error.detailsis always an array. It may be empty ([]), nevernull, never missing.- The
Content-Typeis alwaysapplication/json; charset=utf-8.
How to branch
The decision tree:1
Coarse routing on error.code
2xx → success. 4xx → caller error (do not retry without changing the request). 5xx → server error (may be retryable with backoff).2
Branch on error.status
The canonical reason — e.g.,
SIGNATURE_INVALID, WALLET_NOT_FOUND, IDEMPOTENCY_KEY_IN_USE_WITH_DIFFERENT_PARAMS, RESOURCE_EXHAUSTED. The full catalog is in Error catalog.3
Inspect error.details[].reason for specifics
For validation failures (
INVALID_ARGUMENT), details[] carries one entry per failed field. For sanitized 5xx (INTERNAL), details[] carries the ERROR_RECORDED reference id.4
Capture metadata for support
On any failure you escalate, log
metadata.decisionId (DENY decisions) and metadata.id from ERROR_RECORDED (5xx). These are the handles support uses to trace the failure server-side.A worked example
APOST /wallets/production-main/paymentOrders with a missing field:
- Branch on
error.status === 'INVALID_ARGUMENT'. - Iterate
details[]— there is one entry per invalid field. - For each, read
metadata.fieldto locate the form input; readdescriptionfor the user-facing message. - Do not retry — fix the request and let the user resubmit.
Two patterns to remember
Pattern 1 — simple failure (single cause)
error.status and error.details[0].reason carry the same value.
Pattern 2 — multi-cause validation
error.status is the cross-cutting class (INVALID_ARGUMENT); details[] carries one entry per cause.
details.length before indexing — both patterns are valid; some failures legitimately return empty details.
INTERNAL errors are sanitized
When the API returnsHTTP 500 with error.status: "INTERNAL", the response is deliberately stripped of detail to avoid leaking implementation specifics. What you get:
error.messageis a generic phrase like"An internal error has occurred."details[]carries a singleERROR_RECORDEDentry withmetadata.id(formatexc_…).
metadata.id is gold — it lets BlooBank engineers locate the exact failure server-side. Always log it on INTERNAL and include it when contacting support.
See Errors reference §Sanitized 500s for details.
What to log
For every non-2xx response, log:- HTTP method and URL.
error.statusanderror.code.- Every
details[].reason. metadata.decisionId(DENY decisions) andmetadata.id(ERROR_RECORDED).- Approximate UTC time.
- Your
X-Access-Request-Id.
Next
Error catalog
Every reason code returned by the API, with HTTP status and remediation.
Handling errors
Branching patterns in code, language by language.
Retry strategy
Exponential backoff with jitter — when to retry, when to give up.