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

# Errors

> One envelope, stable codes, and what each one means.

Every error has the same shape:

```json theme={null}
{
  "error": "insufficient_funds",
  "message": "Insufficient funds",
  "details": { "available": "120.00", "required": "150.00", "currency": "GHS" },
  "requestId": "req_q7vei435zifs51ia0lqr"
}
```

`error` is stable and machine-readable — branch on it. `message` is for humans and may change. `details` appears when there's structured context. `requestId` matches the `x-request-id` header.

Validation errors add a `details` array naming each bad field:

```json theme={null}
{
  "error": "validation_error",
  "message": "Request validation failed",
  "details": [{ "field": "to.currency", "message": "Required" }],
  "requestId": "req_…"
}
```

## Codes

| HTTP | `error`                  | Meaning                                                                   | Retry?                          |
| ---- | ------------------------ | ------------------------------------------------------------------------- | ------------------------------- |
| 400  | `validation_error`       | The body failed validation — see `details`                                | No — fix the request            |
| 400  | `bad_request`            | Semantically invalid (e.g. amount on both sides of a payout)              | No                              |
| 400  | `insufficient_funds`     | The balance can't cover the amount + fee                                  | After funding                   |
| 400  | `payout_limit_exceeded`  | A per-transaction, daily, or monthly cap was hit                          | Later, or with a smaller amount |
| 401  | `unauthorized`           | Missing, malformed, expired, or revoked API key                           | No                              |
| 403  | `forbidden`              | Key lacks the scope, or caller IP isn't allowlisted                       | No                              |
| 403  | `organization_suspended` | Contact [api-support@chippercash.com](mailto:api-support@chippercash.com) | No                              |
| 404  | `not_found`              | No such resource in your organization                                     | No                              |
| 409  | `duplicate`              | A resource with this identity already exists (`details.existingId`)       | No — use the existing one       |
| 409  | `conflict`               | The operation conflicts with current state                                | Depends                         |
| 422  | `invalid_recipient`      | Destination failed validation (wrong number, closed account)              | No — fix the destination        |
| 429  | `rate_limited`           | Too many requests (`details.retryAfter` seconds)                          | Yes, after `retryAfter`         |
| 503  | `no_provider_available`  | No live provider for that method right now                                | Yes, with backoff               |
| 503  | `rate_unavailable`       | FX rate temporarily unavailable                                           | Yes, with backoff               |
| 500  | `internal_error`         | Our fault — the `requestId` lets us find it                               | Yes, once                       |

## Handling advice

* Treat `5xx` and `429` as retryable with exponential backoff; everything `4xx` as a bug in the request or a business condition to surface.
* For money-moving retries, **always reuse the same `externalReference`** — see [Idempotency](/concepts/idempotency).
* Payment *failures* are not HTTP errors. A payout the provider rejects returns `201` and later becomes `status: "failed"` with a `statusMessage` — watch the webhook.
