> ## 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.

# Errors

> Canonical error envelope, status codes, reason codes, and how to branch on them.

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](/get-started/errors/handling).

<Note>
  Branch on `error.status` and `error.details[].reason`. **Never string-match `error.message`** — its wording changes between releases.
</Note>

## The envelope

Every error response is a JSON object with exactly one top-level key: `error`.

```json theme={"dark"}
{
  "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

| Field                         | Type    | Stability           | Description                                                                            |
| ----------------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------- |
| `error.code`                  | integer | Stable per `status` | HTTP status code mirrored from the response status line.                               |
| `error.status`                | string  | **Stable**          | Canonical reason identifier in `UPPER_SNAKE_CASE`. Safe to branch on programmatically. |
| `error.message`               | string  | **Unstable**        | Human-readable summary aimed at developers. **Do not string-match.**                   |
| `error.details[]`             | array   | Stable per `reason` | Structured diagnostics. May be empty.                                                  |
| `error.details[].reason`      | string  | **Stable**          | Machine-readable identifier in `UPPER_SNAKE_CASE`. Safe to branch on.                  |
| `error.details[].description` | string  | **Unstable**        | Human-readable explanation of this specific detail.                                    |
| `error.details[].metadata`    | object  | Stable per `reason` | Key/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.code` | Typical `error.status` values                                                                                                           | Meaning                                                                      |
| -----------: | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|        `400` | `INVALID_ARGUMENT`, `MISSING_HEADER`, `TIMESTAMP_INVALID`                                                                               | Request contract violation.                                                  |
|        `401` | `SIGNATURE_INVALID`, `TIMESTAMP_SKEW_EXCEEDED`, `REPLAY_DETECTED`, `CREDENTIAL_DISABLED`, `CREDENTIAL_REVOKED`, `CREDENTIAL_EXPIRED`    | Authentication failure.                                                      |
|        `403` | `RBAC_DENY`, `WORKSPACE_DISABLED`, `SERVICE_ACCOUNT_DISABLED`                                                                           | Authenticated, but not authorized.                                           |
|        `404` | `WALLET_NOT_FOUND`, `PAYMENT_ORDER_NOT_FOUND`                                                                                           | Resource does not exist.                                                     |
|        `409` | `WALLET_ALREADY_EXISTS`                                                                                                                 | Conflict with current state.                                                 |
|        `422` | `IDEMPOTENCY_KEY_IN_USE_WITH_DIFFERENT_PARAMS`, `LABEL_IMMUTABLE`, `PAYMENT_ORDER_NOT_AWAITING_APPROVAL`, `PAYMENT_ORDER_INVALID_STATE` | Well-formed request, but a business precondition failed.                     |
|        `500` | `INTERNAL`                                                                                                                              | Server-side failure. **Sanitized — see [§Sanitized 500s](#sanitized-500s).** |
|        `503` | `PROVIDER_UNAVAILABLE`                                                                                                                  | Downstream dependency temporarily unavailable. Retry with backoff.           |

The full catalog of reason codes returned by this API lives in the [Error catalog](/get-started/errors/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 key     | Source                                | Example                    |
| ---------------- | ------------------------------------- | -------------------------- |
| `metadata.field` | A field inside the JSON request body. | `{ "field": "name" }`      |
| `metadata.param` | A 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

```json theme={"dark"}
{
  "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

```json theme={"dark"}
{
  "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

```json theme={"dark"}
{
  "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.

<Warning>
  **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.
</Warning>

## 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

<CardGroup cols={2}>
  <Card title="Error catalog" icon="list" href="/get-started/errors/error-catalog">
    Every reason code returned by the API, with remediation.
  </Card>

  <Card title="Handling errors" icon="rotate" href="/get-started/errors/handling">
    Branching patterns and retry strategy in code.
  </Card>
</CardGroup>
