# Errors

> One envelope for every response, a stable code in it, and what to do about each one.

Source: https://useroutegate.com/docs/concepts/errors

Every response, success or failure, is the same envelope. Branch on the HTTP status and on `code`; `message` is for people and can change.

```json
{
  "success": true,
  "code": "OK",
  "message": "Transaction accepted",
  "data": { "reference": "txn_01a0d8dfa6b87d10a3e54c2b9f86d1e4", "status": "queued" }
}
```

An error has `success: false`, the code and a message. `data` is `null`:

```json
{
  "success": false,
  "code": "WALLET_INSUFFICIENT_FUNDS",
  "message": "Insufficient wallet balance: available balance (0.00 NGN) is less than required amount (100.00 NGN)",
  "data": null
}
```

A validation error names each field that failed in `data.errors`:

```json
{
  "success": false,
  "code": "VALIDATION_FAILED",
  "message": "Invalid request",
  "data": {
    "errors": [
      {
        "field": "destination",
        "message": "must be a valid 11-digit Nigerian phone number (e.g. 08031234567 or +2348031234567)"
      }
    ]
  }
}
```

Every response also carries an `X-Request-Id` header. Quote it when you ask support about a request.

## Codes

| Code                               | Status | What it means                                                                                                                         | What to do                                                   |
| ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `OK`                               | 2xx    | Success.                                                                                                                              |                                                              |
| `VALIDATION_FAILED`                | 400    | A field is missing or wrong, the body is not one JSON object, or it has a field the endpoint does not take. `data.errors` says which. | Fix the request. Retrying it unchanged gets the same answer. |
| `IDEMPOTENCY_KEY_MISSING`          | 400    | A vend was sent without an `Idempotency-Key` header.                                                                                  | Send one. See [idempotency](/docs/concepts/idempotency).     |
| `AUTH_REQUIRED`                    | 401    | No API key, an invalid, revoked or expired key, or a missing or bad request signature on a key that requires one.                     | Check the key and the mode it belongs to.                    |
| `WALLET_INSUFFICIENT_FUNDS`        | 402    | The wallet's available balance does not cover the price. Nothing was created.                                                         | Fund the wallet, then send the order with a new key.         |
| `PERMISSION_DENIED`                | 403    | The request came from an address outside the key's IP allowlist.                                                                      | Call from an allowed address, or change the allowlist.       |
| `NOT_FOUND`                        | 404    | No such transaction, product or route for this key's business and mode.                                                               | Check the reference and that the key's mode matches.         |
| `CONFLICT`                         | 409    | The `client_reference` is already used by another transaction; the message names it.                                                  | Do not resend. Look the existing transaction up.             |
| `IDEMPOTENCY_IN_PROGRESS`          | 409    | A request with this key is still running.                                                                                             | Retry shortly with the same key.                             |
| `REQUEST_TOO_LARGE`                | 413    | The body is over the size limit.                                                                                                      | Send less, for example fewer `metadata` keys.                |
| `IDEMPOTENCY_FINGERPRINT_MISMATCH` | 422    | The key was already used with a different request.                                                                                    | A different order needs a different key.                     |
| `RATE_LIMITED`                     | 429    | Too many requests for the key or the business. See [rate limits](/docs/concepts/rate-limits).                                         | Wait `Retry-After` seconds, then retry.                      |
| `INTERNAL_ERROR`                   | 500    | Something failed on our side. The message never says more.                                                                            | Retry with the same `Idempotency-Key`.                       |
| `SERVICE_UNAVAILABLE`              | 503    | A dependency or provider is unavailable.                                                                                              | Retry with the same `Idempotency-Key`.                       |

The rest of the registry never comes back from `/api/v1`. `MFA_REQUIRED`, `VERIFICATION_REQUIRED`, `ACCOUNT_LOCKED`, `INVALID_PIN`, `PIN_LOCKED`, `PIN_NOT_SET`, `WEAK_PIN`, `INVALID_PHONE_NUMBER`, `SIM_ALREADY_REGISTERED`, `SIM_UNAUTHORIZED` and `TRANSACTION_INVALID_TRANSITION` belong to the dashboard. `ROUTING_NO_CANDIDATES` is a transaction's `failure_code`, below.

## When a vend fails after 202

A vend that was accepted and then did not deliver is not an HTTP error: the transaction ends `failed`, the hold is released, and `transaction.failed` carries a `failure_code`:

| `failure_code`          | Meaning                                                                                                                                   |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `ROUTING_NO_CANDIDATES` | No line or provider could take the request. In live mode, check that a connected line in a group sells the plan and has the stock for it. |
| `PROVIDER_REJECTED`     | The channel refused it outright, for example an invalid destination.                                                                      |
| `PROVIDER_FAILED`       | The channel failed it, or a status query found it failed.                                                                                 |
| `RETRIES_EXHAUSTED`     | Every route the plan allowed was tried without success.                                                                                   |

One code appears on a transaction that has not failed. `PENDING_TIMEOUT` marks a transaction still `pending` when its status queries ran out: it stays `pending`, in manual review with the hold kept, until staff settle it one way or the other.
