> For the complete documentation index, see [llms.txt](https://docs.fonbnk.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fonbnk.com/server-to-server/errors.md).

# Errors

What a refused request tells you, and what to do about it.

Every failed request answers with the same body. Read `errorCode`, not the text: the text is written for a person and may be reworded, the code is a contract.

```typescript
type ErrorResponse = {
  message: string;                      // one sentence, safe to show a user
  errorCode?: string;                   // the stable reason — branch on this
  details?: Record<string, unknown>;    // the same answer as data
  validation?: { errors: string[] };    // one entry per failed field, on a 400
}
```

`details` carries what you need to correct the request without parsing the sentence. Every code below sets `leg` (`deposit` or `payout`) and `field` (for example `deposit.amount`), and the amount codes add `min`, `max`, `step`, `currencyCode` and sometimes `derived`. Get order limits carries the same object inside its `unsupportedReason`, so one `errorCode` has one shape wherever it reaches you.

Example:

```json
{
    "message": "The deposit amount is below the minimum of 1494 NGN. Call GET /api/v2/order-limits for the amount limits of this corridor.",
    "errorCode": "AMOUNT_BELOW_MINIMUM",
    "details": {
        "leg": "deposit",
        "field": "deposit.amount",
        "min": 1494,
        "max": 747367,
        "currencyCode": "NGN"
    }
}
```

`min` and `max` appear only when there is a real bound to give. A route with no minimum, or no maximum, leaves the field out rather than reporting a zero.

{% hint style="warning" %}
**`details.derived: true` means `field` names an input you did not send.** An order carries one amount. Whichever leg you priced, the other one is searched on that amount converted to USD — so a request carrying `deposit.amount` in NGN can be refused by a USD bound on `payout.amount`. The field to change is the one you sent, on the other leg. Read `derived` before you point a form highlighter at `field`.

The conversion runs both ways: pricing the deposit checks the payout leg against what is **left** after the deposit fees, and pricing the payout checks the deposit leg against the gross it **needs** before them.
{% endhint %}

## No offer matched — `404`

Create quote and Create order search for a pair of offers that can serve your request. When nothing matches, the code says which part of the request has no offer behind it.

<table><thead><tr><th width="270">errorCode</th><th>What it means</th><th>What to do</th></tr></thead><tbody><tr><td><code>CURRENCY_TYPE_NOT_SUPPORTED</code></td><td>A real currency type, with no route behind it on that leg</td><td>The message lists the types that have one</td></tr><tr><td><code>CURRENCY_CODE_NOT_SUPPORTED</code></td><td>The currency type exists, the code does not</td><td>Read Get available currencies</td></tr><tr><td><code>COUNTRY_NOT_SUPPORTED</code></td><td>We do not sell that currency in that country</td><td>The message lists the countries we do</td></tr><tr><td><code>PAYMENT_CHANNEL_NOT_SUPPORTED</code></td><td>That channel does not serve that currency and country</td><td>The message lists the channels that do</td></tr><tr><td><code>CARRIER_NOT_SUPPORTED</code></td><td>That carrier does not run on that channel</td><td>The message lists the carriers that do</td></tr><tr><td><code>TRANSFER_TYPE_NOT_SUPPORTED</code></td><td>That transfer type is not offered on that channel</td><td>Leave <code>transferType</code> out, or use a listed one</td></tr><tr><td><code>QUOTE_REQUIRED</code></td><td>The route works and is kept out of ordinary search on purpose</td><td>Use another channel, or contact us</td></tr><tr><td><code>ROUTE_NOT_AVAILABLE</code></td><td>The corridor exists and is not usable right now</td><td>Retry later, or offer another channel</td></tr><tr><td><code>AMOUNT_BELOW_MINIMUM</code></td><td>Under the smallest amount any route takes</td><td><code>details.min</code></td></tr><tr><td><code>AMOUNT_ABOVE_MAXIMUM</code></td><td>Over the largest amount any route takes</td><td><code>details.max</code></td></tr><tr><td><code>AMOUNT_STEP_INVALID</code></td><td>The amount breaks the step every route requires</td><td><code>details.step</code></td></tr><tr><td><code>AMOUNT_NOT_COVERED</code></td><td>Inside the overall window, in a gap between two routes</td><td>Read Get order limits and pick another amount</td></tr></tbody></table>

{% hint style="info" %}
`ROUTE_NOT_AVAILABLE` is the only one of these that is worth retrying. Every other code means the request will fail again unchanged.
{% endhint %}

### `QUOTE_REQUIRED`

Some routes are kept out of ordinary offer search on purpose. In production that is a route Fonbnk has restricted. **In sandbox this covers almost everything**, because every real fiat offer is forced quote-only there, so a corridor with no sandbox provider behind it answers this code.

No call of yours opens the route. Create quote runs the same search and answers the same code, so a retry with a quote does not help. Use another payment channel, or contact Fonbnk support to have the route opened for your integration.

### `AMOUNT_NOT_COVERED`

The one you cannot fix by arithmetic on `min` and `max`. The amount falls between two routes: one runs to 100, the next starts at 500, and you asked for 300. **It carries no `min` and no `max` on purpose** — those are the union across every route, so 101 to 499 sits inside them and is refused all the same. Get order limits reconciles both legs and is the number to aim at.

## Only from Get order limits — `200`

<table><thead><tr><th width="270">errorCode</th><th>What it means</th><th>What to do</th></tr></thead><tbody><tr><td><code>LIMITS_DO_NOT_OVERLAP</code></td><td>The two sides cannot meet: both legs have routes and no amount satisfies both, or one side publishes no usable bounds, or a balance sits under every minimum</td><td>Nothing to read. Use another corridor, or contact us</td></tr></tbody></table>

This one never rides a `404`. It arrives inside `unsupportedReason` on a `200`, and it is the one code with nowhere to send you: the corridor is in the currency list and its window is the zeros you are already reading. Several different dead ends raise it, so read the message rather than the code alone. The usual cause is a per-transaction limit on your account that leaves the deposit floor above the payout ceiling.

## The request itself is wrong — `400`

<table><thead><tr><th width="270">errorCode</th><th>What it means</th></tr></thead><tbody><tr><td><code>CURRENCY_TYPE_INVALID</code></td><td>That <code>currencyType</code> is not one we route at all. The message lists the ones we do. Not the same as <code>CURRENCY_TYPE_NOT_SUPPORTED</code> above, which is a real type with no route behind it right now.</td></tr><tr><td><code>CURRENCY_PAIR_NOT_SUPPORTED</code></td><td>We do not pay out in that currency type for that deposit currency type. The message lists the payout types we do.</td></tr><tr><td><code>COUNTRY_REQUIRED</code></td><td>A <code>fiat</code> leg carries no <code>countryIsoCode</code>.</td></tr><tr><td><code>AMOUNT_REQUIRED</code></td><td>Neither leg carries an amount.</td></tr><tr><td><code>AMOUNT_AMBIGUOUS</code></td><td>Both legs carry an amount. Send exactly one.</td></tr></tbody></table>

A `400` carries no `details`. A field that fails its own validation answers `400` with no `errorCode` either, and a `validation.errors` array instead, one entry per failed field.

{% hint style="info" %}
**Send exactly one amount, and do not send `0` for the other.** `{"deposit": {"amount": 0}, "payout": {"amount": 500}}` used to be accepted and then searched for an amount of nothing, which answered `404`. It is refused as `400` now. Leave the leg you are not pricing without an `amount` key.
{% endhint %}

## Rate limited — `429`

A `429` carries a `Retry-After` header in seconds and a `retryAfter` field in the body, in epoch milliseconds. Wait the stated time. `errorCode` names the limit that was hit.

## Authentication — `401`

The signature, the client id or the timestamp did not check out. See Signing requests. A timestamp more than a few minutes from ours is the most common cause.

## What we will not change

`errorCode` values are stable. We may add a new code, so treat an unknown code as a generic failure and read `message`. We may also reword any `message`, so do not match on its text.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.fonbnk.com/server-to-server/errors.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
