For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

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:

{
    "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.

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.

errorCode
What it means
What to do

CURRENCY_TYPE_NOT_SUPPORTED

A real currency type, with no route behind it on that leg

The message lists the types that have one

CURRENCY_CODE_NOT_SUPPORTED

The currency type exists, the code does not

Read Get available currencies

COUNTRY_NOT_SUPPORTED

We do not sell that currency in that country

The message lists the countries we do

PAYMENT_CHANNEL_NOT_SUPPORTED

That channel does not serve that currency and country

The message lists the channels that do

CARRIER_NOT_SUPPORTED

That carrier does not run on that channel

The message lists the carriers that do

TRANSFER_TYPE_NOT_SUPPORTED

That transfer type is not offered on that channel

Leave transferType out, or use a listed one

QUOTE_REQUIRED

The route works and is kept out of ordinary search on purpose

Use another channel, or contact us

ROUTE_NOT_AVAILABLE

The corridor exists and is not usable right now

Retry later, or offer another channel

AMOUNT_BELOW_MINIMUM

Under the smallest amount any route takes

details.min

AMOUNT_ABOVE_MAXIMUM

Over the largest amount any route takes

details.max

AMOUNT_STEP_INVALID

The amount breaks the step every route requires

details.step

AMOUNT_NOT_COVERED

Inside the overall window, in a gap between two routes

Read Get order limits and pick another amount

ROUTE_NOT_AVAILABLE is the only one of these that is worth retrying. Every other code means the request will fail again unchanged.

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

errorCode
What it means
What to do

LIMITS_DO_NOT_OVERLAP

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

Nothing to read. Use another corridor, or contact us

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

errorCode
What it means

CURRENCY_TYPE_INVALID

That currencyType is not one we route at all. The message lists the ones we do. Not the same as CURRENCY_TYPE_NOT_SUPPORTED above, which is a real type with no route behind it right now.

CURRENCY_PAIR_NOT_SUPPORTED

We do not pay out in that currency type for that deposit currency type. The message lists the payout types we do.

COUNTRY_REQUIRED

A fiat leg carries no countryIsoCode.

AMOUNT_REQUIRED

Neither leg carries an amount.

AMOUNT_AMBIGUOUS

Both legs carry an amount. Send exactly one.

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.

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.

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.