> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usenumero.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors & Responses

> One envelope, real HTTP status codes, and a stable error code on every failure — so you can branch on structure instead of parsing prose.

Every response from the Numero API uses the same envelope, and the HTTP status code always tells you
what happened. You never have to read a message to find out whether a request succeeded.

## The envelope

```json theme={null}
{
  "data": { },
  "error": null,
  "meta": {
    "request_id": "req_365123ebc73323f1482508560ce6400e",
    "pagination": null
  }
}
```

| Field             | Type                    | Description                                                                                                    |
| ----------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| `data`            | object \| array \| null | The payload on success. `null` on failure.                                                                     |
| `error`           | object \| null          | `null` on success. On failure, carries `code`, `message` and `param`.                                          |
| `meta.request_id` | string                  | Unique id for this request. **Quote it when you contact support.** Also returned in the `X-Request-Id` header. |
| `meta.pagination` | object \| null          | Present on list endpoints — see [Pagination](#pagination).                                                     |

`data` and `error` are mutually exclusive: exactly one of them is non-null on every response.

## Failures

```json theme={null}
{
  "data": null,
  "error": {
    "code": "insufficient_balance",
    "message": "Your wallet balance is too low for this transfer.",
    "param": "amount"
  },
  "meta": {
    "request_id": "req_365123ebc73323f1482508560ce6400e",
    "pagination": null
  }
}
```

| Field           | Description                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------- |
| `error.code`    | **Stable, machine-readable.** Branch on this. It will not change for a given condition.        |
| `error.message` | Human-readable, for your logs and support tickets. **Wording may change — never match on it.** |
| `error.param`   | The offending field, when the failure is about one. `null` otherwise.                          |

<Warning>
  Branch on `error.code`, never on `error.message`. Messages are edited for clarity over time; codes are
  part of the contract.
</Warning>

## HTTP status codes

The status code is real — a failed request never arrives as `200`. Checking `response.ok` (or
`response.status < 400`) is a correct success test.

| Status | Meaning            | What to do                                                                                                                                           |
| ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Success            | Read `data`.                                                                                                                                         |
| `201`  | Created            | The resource exists now.                                                                                                                             |
| `202`  | Accepted           | Work is in flight; poll the relevant status endpoint or wait for the webhook.                                                                        |
| `400`  | Bad request        | Something about the request is wrong — missing header, bad field, failed validation. Fix and retry.                                                  |
| `401`  | Unauthenticated    | The API key is missing or not recognised.                                                                                                            |
| `403`  | Forbidden          | Authenticated, but not allowed — missing API-key scope, IP not allow-listed, or product not enabled. A rejected *signature* is a `400`, not a `403`. |
| `404`  | Not found          | No such resource, or no endpoint at this path and method.                                                                                            |
| `405`  | Method not allowed | Wrong HTTP verb for this path.                                                                                                                       |
| `409`  | Conflict           | An `Idempotency-Key` was reused with a different body — see [Idempotency](/06-idempotency).                                                          |
| `413`  | Payload too large  | The body exceeds the limit for this endpoint.                                                                                                        |
| `422`  | Unprocessable      | Well-formed, but rejected by a business rule.                                                                                                        |
| `429`  | Rate limited       | Too many requests. Honour the `Retry-After` header.                                                                                                  |
| `5xx`  | Server error       | Our side. Safe to retry with the same `Idempotency-Key`.                                                                                             |

### Examples by status

**400 — missing API key**

```json theme={null}
{
  "data": null,
  "error": { "code": "missing_field", "message": "Header key is missing.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**400 — invalid API key**

```json theme={null}
{
  "data": null,
  "error": { "code": "invalid_api_key", "message": "Invalid API key.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**400 — signature rejected**

A rejected signature, a missing signature header and a missing API key all arrive as `400` with a
distinct `error.code`. Branch on the code, not the status.

```json theme={null}
{
  "data": null,
  "error": { "code": "invalid_signature", "message": "Invalid signature.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**404 — no such endpoint**

```json theme={null}
{
  "data": null,
  "error": { "code": "not_found", "message": "No endpoint matches this path and method.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**409 — idempotency conflict**

```json theme={null}
{
  "data": null,
  "error": { "code": "idempotency_conflict", "message": "This Idempotency-Key was already used with a different request body.", "param": "Idempotency-Key" },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**429 — rate limited**

```json theme={null}
{
  "data": null,
  "error": { "code": "rate_limited", "message": "Too many requests. Try again shortly.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

**500 — server error**

```json theme={null}
{
  "data": null,
  "error": { "code": "internal_error", "message": "The request could not be completed.", "param": null },
  "meta": { "request_id": "req_a1b2c3", "pagination": null }
}
```

## Error codes

Grouped by what you should do about them.

**Authentication & authorization** — fix your credentials; retrying unchanged will not help.

`missing_field` · `invalid_api_key` · `invalid_signature` · `unauthorized` · `forbidden`

**Your request** — fix the input, then retry.

`invalid_request` · `validation_failed` · `invalid_amount` · `invalid_account_number` · `invalid_bank` ·
`invalid_bvn` · `invalid_pin` · `pin_not_set` · `invalid_otp` · `otp_expired` · `otp_required` ·
`already_exists` · `duplicate_request` · `idempotency_conflict` · `not_found` · `not_implemented`

**Your account's state** — needs an action on your side, often in the dashboard.

`insufficient_balance` · `business_not_found` · `business_inactive` · `kyc_required` ·
`approval_required` · `activation_required` · `limit_exceeded` · `rate_limited`

**The resource** — the thing you named does not exist under your business.

`account_not_found` · `customer_not_found` · `recipient_not_found` · `transaction_not_found` ·
`virtual_account_not_found`

**Downstream** — usually transient. Retry with the **same** `Idempotency-Key`.

`provider_error` · `provider_timeout` · `provider_unavailable` · `internal_error`

<Note>
  A money-movement request that fails with a downstream code may still have reached the provider. Retry
  with the same `Idempotency-Key` — that is exactly what it is for — or query the relevant status
  endpoint before assuming it did not happen.
</Note>

## Handling errors

```javascript theme={null}
const res = await fetch(url, { method: "POST", headers, body });
const payload = await res.json();

if (!res.ok) {
  const { code, message, param } = payload.error;
  const requestId = payload.meta.request_id;

  // Branch on code, never on message.
  switch (code) {
    case "insufficient_balance": return topUpAndRetry();
    case "rate_limited":         return retryAfter(res.headers.get("Retry-After"));
    case "idempotency_conflict": return reconcile(requestId);
    default:
      throw new Error(`Numero ${code}: ${message} (request_id=${requestId}${param ? `, param=${param}` : ""})`);
  }
}

return payload.data;
```

Log `meta.request_id` on every failure. It is the one value that lets support find your exact request.

## Pagination

List endpoints accept `pageNumber` and `pageSize`:

| Parameter    | Type    | Default | Description                |
| ------------ | ------- | ------- | -------------------------- |
| `pageNumber` | integer | 1       | The page to retrieve       |
| `pageSize`   | integer | 50      | Records per page (max 200) |

On a paginated endpoint, `data` is the **array of records itself** and the page details are lifted into
`meta.pagination`:

```json theme={null}
{
  "data": [ ],
  "error": null,
  "meta": {
    "request_id": "req_365123ebc73323f1482508560ce6400e",
    "pagination": { "page": 1, "page_size": 50, "total": 150, "has_more": true }
  }
}
```

| Field       | Type    | Description                                                  |
| ----------- | ------- | ------------------------------------------------------------ |
| `page`      | integer | The page you received                                        |
| `page_size` | integer | Records per page                                             |
| `total`     | integer | Total records matching the query                             |
| `has_more`  | boolean | `true` when another page exists — page until this is `false` |

## A note on older integrations

Some Numero endpoints outside this reference — the ones the Numero dashboards themselves call — still
answer with an older envelope (`status`, `message`, `code`, `reference`) and report failures inside a
`200` response. **That shape is not part of the developer API and is not supported for integrations.**
Everything documented in this reference uses the envelope and status codes above.
