Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.bloobank.com/llms.txt

Use this file to discover all available pages before exploring further.

Every BlooBank error response uses the same JSON envelope, regardless of which endpoint produced it. This page is the reference contract — the developer-facing guide on how to react to errors lives in Errors → Handling.
Branch on error.status and error.details[].reason. Never string-match error.message — its wording changes between releases.

The envelope

Every error response is a JSON object with exactly one top-level key: error.
{
  "error": {
    "code": 404,
    "status": "WALLET_NOT_FOUND",
    "message": "Wallet not found.",
    "details": [
      {
        "reason": "WALLET_NOT_FOUND",
        "description": "No wallet exists with name `production-main`.",
        "metadata": {}
      }
    ]
  }
}
The envelope shape is invariant:
  • error is always present.
  • error.code, error.status, error.message, and error.details are always present.
  • error.details is always an array. It may be empty ([]), but it is never null and never missing.
  • The response Content-Type is always application/json; charset=utf-8.

Field reference

FieldTypeStabilityDescription
error.codeintegerStable per statusHTTP status code mirrored from the response status line.
error.statusstringStableCanonical reason identifier in UPPER_SNAKE_CASE. Safe to branch on programmatically.
error.messagestringUnstableHuman-readable summary aimed at developers. Do not string-match.
error.details[]arrayStable per reasonStructured diagnostics. May be empty.
error.details[].reasonstringStableMachine-readable identifier in UPPER_SNAKE_CASE. Safe to branch on.
error.details[].descriptionstringUnstableHuman-readable explanation of this specific detail.
error.details[].metadataobjectStable per reasonKey/value context. Schema depends on the reason.

error.status vs error.details[].reason

Both carry stable UPPER_SNAKE_CASE identifiers. The practical rule:
  • For simple failures with a single cause (most cases), error.status and error.details[0].reason carry the same value (e.g., both WALLET_NOT_FOUND).
  • For validation failures with multiple field issues, error.status is the cross-cutting class (INVALID_ARGUMENT) and error.details[] carries one entry per invalid field.
Always inspect both. error.status is the coarse classifier; details[].reason is the specific cause.

Status to HTTP mapping

The mapping is fixed.
error.codeTypical error.status valuesMeaning
400INVALID_ARGUMENT, MISSING_HEADER, TIMESTAMP_INVALIDRequest contract violation.
401SIGNATURE_INVALID, TIMESTAMP_SKEW_EXCEEDED, REPLAY_DETECTED, CREDENTIAL_DISABLED, CREDENTIAL_REVOKED, CREDENTIAL_EXPIREDAuthentication failure.
403RBAC_DENY, WORKSPACE_DISABLED, SERVICE_ACCOUNT_DISABLEDAuthenticated, but not authorized.
404WALLET_NOT_FOUND, PAYMENT_ORDER_NOT_FOUNDResource does not exist.
409WALLET_ALREADY_EXISTSConflict with current state.
422IDEMPOTENCY_KEY_IN_USE_WITH_DIFFERENT_PARAMS, LABEL_IMMUTABLE, PAYMENT_ORDER_NOT_AWAITING_APPROVAL, PAYMENT_ORDER_INVALID_STATEWell-formed request, but a business precondition failed.
500INTERNALServer-side failure. Sanitized — see §Sanitized 500s.
503PROVIDER_UNAVAILABLEDownstream dependency temporarily unavailable. Retry with backoff.
The full catalog of reason codes returned by this API lives in the Error catalog.

Validation errors (INVALID_ARGUMENT)

The most common error during integration is request validation. The platform formats these consistently across every endpoint.
  • error.status is "INVALID_ARGUMENT".
  • error.code is 400.
  • error.message is a generic summary, e.g. "One or more fields have invalid values.".
  • error.details[] contains one entry per failed constraint.

field vs param

The platform distinguishes body fields from query/path parameters.
Metadata keySourceExample
metadata.fieldA field inside the JSON request body.{ "field": "name" }
metadata.paramA query-string or path parameter.{ "param": "page_size" }
Each detail carries exactly one of these. Use it to locate the offending input in your request.

Example — multiple invalid fields

{
  "error": {
    "code": 400,
    "status": "INVALID_ARGUMENT",
    "message": "One or more fields have invalid values.",
    "details": [
      {
        "reason": "INVALID_FIELD",
        "description": "The field \"name\" must be a valid DNS label.",
        "metadata": { "field": "name", "constraint": "dns_label" }
      },
      {
        "reason": "INVALID_FIELD",
        "description": "The field \"status\" must be one of: ACTIVE, DISABLED.",
        "metadata": { "field": "status" }
      }
    ]
  }
}

Example — invalid query parameter

{
  "error": {
    "code": 400,
    "status": "INVALID_ARGUMENT",
    "message": "The page_size parameter is out of range.",
    "details": [
      {
        "reason": "INVALID_PAGE_SIZE",
        "description": "The parameter \"page_size\" must be between 1 and 100.",
        "metadata": { "param": "page_size", "max": 100 }
      }
    ]
  }
}
Query-parameter errors may use a more specific reason than INVALID_FIELD (e.g., INVALID_PAGE_SIZE, INVALID_FILTER, INVALID_ORDER_BY, INVALID_PAGE_TOKEN).

Sanitized 500s

Responses with error.status: "INTERNAL" receive special treatment.

What sanitization means

When a service produces an internal error, the platform mutates the response before sending it:
  • The original error.message is replaced with a generic phrase such as "An internal error has occurred.".
  • Any original details[] entries are dropped.
  • A single details[] entry with reason: "ERROR_RECORDED" is appended (when the audit record was persisted successfully).
This is intentional and non-negotiable — internal errors may carry implementation details that must not cross the network boundary.

Anatomy of a recorded internal error

{
  "error": {
    "code": 500,
    "status": "INTERNAL",
    "message": "An internal error has occurred.",
    "details": [
      {
        "reason": "ERROR_RECORDED",
        "description": "An unexpected error has occurred. Please contact support and provide the reference id \"exc_9RmKvTpYzH2wN8qJ5b\".",
        "metadata": {
          "id": "exc_9RmKvTpYzH2wN8qJ5b"
        }
      }
    ]
  }
}
The metadata.id (format exc_…) is the single most valuable piece of information you can forward to support. With it, a BlooBank engineer can locate the full audit trail in seconds.

What to do on INTERNAL

  1. Locate the detail with reason === "ERROR_RECORDED".
  2. Extract metadata.id.
  3. Log it with the URL, method, approximate UTC time, and your X-Access-Request-Id.
  4. Forward it when escalating.
If ERROR_RECORDED is missing (rare — the audit-record persistence itself failed), send the approximate UTC time and your request id; engineers can still trace via access logs.
Do not retry INTERNAL blindly. It indicates a known backend failure that the service understands but cannot resolve for you. Retry only if you have explicit reason to believe it was transient.

What is not in the envelope

Do not depend on these — they may appear or disappear between releases:
  • Top-level fields outside error (timestamp, path, traceId at the root). Treat as informational.
  • Nested fields inside error other than the four documented above.
  • Stack traces. Never returned, regardless of failure mode.

Next

Error catalog

Every reason code returned by the API, with remediation.

Handling errors

Branching patterns and retry strategy in code.