OnLink

Errors

One error envelope for every refusal, the codes you branch on, and what is worth retrying.

Every partner-plane failure returns the same JSON shape.

{
  "statusCode": 409,
  "message": "QUOTE_EXPIRED_OR_CONSUMED",
  "error": "Conflict",
  "requestId": "req-1756512000000-a1b2c3d"
}

Two fields are appended when they apply:

  • errorCode — present on some refusals, absent on most.
  • retryAfterSeconds — present only when a refusal supplies one. The rate limit does not: its 429 carries the wait in the Retry-After-partner header and nothing in the body.

Branch on message, not on errorCode

The stable short codes arrive in message. errorCode is only set on some refusals, so a client that switches on it silently falls through to a default branch for most of the catalogue. Validation failures are the one exception to the short-code shape: they read Validation failed on: <fields>.

requestId is on every error body and is also returned as the X-Request-Id header. It is the only handle that resolves to your request in our logs — log it on every non-2xx response and quote it when you contact us.

Status codes

StatusMeaningRetry?
202Accepted. The order exists; it has not settled.—
400The request body failed validation.No. Fix the request.
401Authentication failed. Uniform, with no reason given.No. See below.
403Your credential is authenticated but not authorised for this endpoint.No. Ask us for the scope.
404No such route, or no such order for your partner account.No.
409A conflict with existing state — a spent quote, a reused reference, a hash already attached.Only after changing something.
422The order breaches one of your caps, or your KES balance cannot cover it.No. Fix the amount, or fund your KES balance, then create a new order.
429Rate limited.Yes, after the Retry-After-partner header.
503Your account readiness, the settlement account, or a platform-wide control is not ready right now.Depends on the code. See below.

401 is deliberately uninformative

A bad signature, an expired timestamp, a replayed nonce and an unknown key all return the same body:

{
  "statusCode": 401,
  "message": "An error occurred",
  "error": "unauthorized",
  "requestId": "req-1756512061412-9f4e2b7"
}

Distinguishing them would tell an attacker which half of a guess was right. Debug against the worked example on the authentication page instead.

429 carries its own backoff

{
  "statusCode": 429,
  "message": "Too Many Requests",
  "error": "Too Many Requests",
  "requestId": "req-1756512184903-7c1d5a0"
}

Read the wait, in seconds, from the Retry-After-partner header. The limit is enforced by a throttler named partner, and the header carries that name. The body has no retryAfterSeconds on this refusal, so a client that looks only at the body backs off for nothing and is refused again. The limit is 120 requests per minute per partner across all your credentials — see Caps and limits.

The code catalogue

These are the values that arrive in message. Branch on them.

Every row carries the code, the status it comes back on, what it means and what most often causes it, and what to do. Read the last column first: it opens with the retry verdict, because that is the decision your code has to make before anything else.

Quotes

CodeStatusWhat it meansWhat to do
QUOTE_EXPIRED_OR_CONSUMED409The quote has expired or already made an order. Usually a user who hesitated past expiresAt, or a create retried on a spent quote.No. Take a fresh quote — the same one refuses forever.
QUOTE_SIDE_MISMATCH409A buy quote was presented to the sell route, or the reverse. Usually one code path builds the quote and another picks the route.No. Quote the side you are creating, then retry.

Fees

CodeStatusWhat it meansWhat to do
FEE_EXCEEDS_PROCEEDS422On a sell, the fee equals or exceeds the KES the trade would pay out, so there would be nothing left to send you. Reached on very small sells against a flat fee — see Fees.No, not at this size. Quote a larger amount. Retrying the same amount refuses identically, and a buy of the same size still prices.
FEE_PRICING_INVALID503Your stored pricing cannot be applied, so we will not guess at a fee. Ours to fix, not yours.Yes, after we fix it. Nothing about your request caused this — contact us; retrying sooner gets the same answer.

Orders

CodeStatusWhat it meansWhat to do
ORDER_EXCEEDS_PER_ORDER_CAP422This one order is above the per-order ceiling set for your account.No. Split it, or ask us to review the cap.
ORDER_EXCEEDS_DAILY_CAP422It would breach your rolling 24-hour total. Caused by volume earlier in the window, not by this order alone. The window now counts committed USDT sends as well as orders and transfers.Not yet. Wait for the window to roll, or talk to us.
ORDER_DAILY_CAP_RATE_UNAVAILABLE503The daily-cap check needed a live market rate to convert your committed USDT sends into KES, and none was available. Only affects an account with committed sends outstanding.Yes. Retry shortly.
ORDER_NOT_BUY_SIDE409A buy-only action was taken against a sell order. Usually the wrong orderId, or one handler serving both sides.No. Check the orderId first.
ORDER_NOT_SELL_SIDE409A sell-only action was taken against a buy order. Usually the wrong orderId, or one handler serving both sides.No. Check the orderId first.
ORDER_NOT_AWAITING_USDT409A hash was attached to an order no longer awaiting a deposit. Often a retry that raced the first attach.No. Read the order; it has already moved on.
EXPECTED_AMOUNT_MISMATCH409expectedUsdtAmount on a buy order did not equal the quote's usdtAmount. Usually an amount recomputed on your side instead of read off the quote.No. Pass the quote's usdtAmount verbatim, or omit the field — it asserts, it does not set.
PARTNER_SUSPENDED409Your partner account cannot create orders. A suspension, not a problem with this request.No. Contact us; retrying will not clear it.

Account provisioning

Your KES collection account — the number a payer sends M-PESA or a bank transfer to on the buy leg — is derived from your partner account rather than configured. These codes cover the cases where it is not yet ready.

CodeStatusWhat it meansWhat to do
PARTNER_VA_NOT_ALLOCATED503No KES collection account is linked to your partner account yet, because onboarding is not finished.No. Ask us to link a verified business, or to open one.
PARTNER_VA_ACTIVATING503The account exists but has not finished activating. It resolves on its own.Yes. The one code here worth a short retry loop.
PARTNER_VA_SUSPENDED503Your KES collection account has been suspended. A suspension, not a problem with this request.No. Contact us; retrying will not clear it.
PARTNER_NOT_FOUND404Your partner account could not be resolved. Not something your integration can cause.No. Contact us with the requestId.

Buy leg (debit-first)

POST /v1/orders/buy debits your own KES balance the instant the order is created — see Buy USDT with KES. These codes cover every way that debit can be refused, in the order the checks run.

CodeStatusMeaningWhat to do
SWAPS_DISABLED503Partner USDT/KES swaps are disabled platform-wide right now.Retry later. Nothing about your request is wrong.
KILL_SWITCH_ENGAGED503An operator has halted all USDT/KES value movement as an incident control.Do not retry in a tight loop. Wait for us to confirm the incident has cleared.
KES_BALANCE_UNREADABLE503We could not read your KES balance to check it covers the order.Retry shortly. If it persists, contact us with the requestId.
INSUFFICIENT_KES_BALANCE422Your available KES balance is below the quoted amount.Fund your KES balance, then retry — this refusal happens before your quote is consumed.
TREASURY_ACCOUNT_UNAVAILABLE503The settlement account was unavailable — either checked before or immediately before the debit. It may be transiently unavailable, or its registration has been retired or replaced entirely.Retry once, briefly. If it recurs, stop retrying and contact us with the requestId — a retired registration will not clear on its own.
TREASURY_ACCOUNT_NOT_REGISTERED503No settlement account is registered for this currency at all.Contact us with the requestId. Retrying will not clear it.
KES_DEBIT_FAILED503We attempted the debit and the bank cleanly refused it — a validated business rejection, not a timeout. The order is now rejected.Read the order to confirm, then create a new order against a fresh quote. Do not retry this request.
KES_DEBIT_AMBIGUOUS503The KES debit result is ambiguous — we cannot tell whether it succeeded or failed, or we cannot record the outcome. Your KES balance may already have been debited. The order stays funds_reserved while we resolve it from the bank's record.Do not retry. Read the order via GET /v1/orders/{orderId} to see the outcome once we have resolved it.

This table covers the SYNCHRONOUS refusals only

Both KES_DEBIT_FAILED and a late TREASURY_ACCOUNT_UNAVAILABLE (our own re-check, run immediately before the debit) reject the order rather than leaving it stranded, and for both of these nothing was ever taken — the debit was refused before it reached the bank, or never attempted at all. That is true of these two codes and only these two. A rejected order you learn about later — through order.rejected or by polling — can also arrive after the bank accepted your debit, and that case may carry a reversal in progress rather than nothing having moved. See Buy USDT with KES for that distinction, and read kesDebit.status on the order — never assume from the word "rejected" alone.

Sell leg (USDT collection)

POST /v1/orders/sell collects the USDT from your own wallet before anything is owed to you. These codes cover every way that collection can be refused. All of them happen at create, before any KES is paid out.

CodeStatusMeaningWhat to do
CUSTOMER_WALLETS_DISABLED503The partner-customer wallets rail is switched off right now. Checked first, before any wallet call and any debit.Retry later. Nothing about your request is wrong, and nothing was collected.
INSUFFICIENT_USDT_BALANCE422Your available USDT balance is below the order amount. The order is now rejected.Fund the wallet, then create a new order against a fresh quote.
USDT_COLLECTION_FAILED503We attempted the collection and it was refused. The order is now rejected.Read the order to confirm, then create a new order against a fresh quote. Do not retry this request.
WALLET_OWNERSHIP_CHECK_FAILED503The wallet named on the order did not pass our ownership check, so no collection was attempted.Stop retrying and contact us with the requestId — this does not clear on its own.
BRIDGE_NOT_CONFIGURED503Our own custody configuration for this rail is incomplete. Entirely ours, and nothing about your request caused it.Contact us with the requestId. Retrying will not clear it.

A refusal here means nothing was collected

Every code in this table is raised before or instead of a successful collection, so your USDT balance is untouched in all five cases. The two that move the order to rejected (INSUFFICIENT_USDT_BALANCE and USDT_COLLECTION_FAILED) do so precisely because the attempt is finished — there is nothing outstanding to resolve, and a new order is the way forward.

Deposits and payouts

CodeStatusWhat it meansWhat to do
TX_HASH_ALREADY_ATTACHED409This order already has a hash. Usually an attach retried after the first one succeeded.No. One hash per order; the order is already funded.
TX_HASH_ALREADY_USED409That hash is attributed to another order. Usually one deposit meant to cover two.No. One deposit funds one order. Send another.
PAYOUT_ACCOUNT_NOT_PAYABLE409The payout account cannot take a credit — closed, frozen or otherwise unable.No. Use another registered account, or contact us.
MPESA_PUSH_REJECTED422The bank answered and refused this request. Usually a number that is not M-PESA-registered.No. The same request cannot succeed. Fix the number.
MPESA_PUSH_UNAVAILABLE503We got no answer from the bank. The prompt may or may not have reached the payer.Carefully — see below.

These two are the failure modes of POST /v1/funding/mpesa-push, and the difference between them is the whole point of having two codes.

MPESA_PUSH_REJECTED is the bank answering. Your request was understood and declined, so retrying it unchanged will be declined again — and every attempt is a real request. The most common cause by far is a number that is not M-PESA-registered. It does not name the upstream condition: a mobile-money rail has failure modes that are ours to read and not yours to branch on, and a code that changed whenever an upstream reworded an error would be worse than one that does not.

MPESA_PUSH_UNAVAILABLE is the bank not answering, and it is genuinely ambiguous: the request may have arrived and prompted the payer before the connection failed. There is no idempotency key on this route, so an immediate retry can raise a second prompt on the same handset — and if the payer approves both, two deposits land. Prefer waiting for credit.received; retry only if no credit arrives. If it persists, send us the requestId.

Everything else that can refuse a push is a 400 on the body — including an amount above the M-PESA per-transaction limit, which is refused here rather than forwarded — or one of the account codes above. The destination is your own account and is resolved from your partner record, so there is nothing about it for you to get wrong.

Scope

A 403 carries no code from this catalogue. error is the constant insufficient_scope and errorCode names the scope the route needed, so the refusal tells you what to ask for. See Products and scopes.

Ignore codes you do not recognise

We may add codes. Treat an unknown message on a 4xx as a non-retryable refusal, log it with its requestId, and surface it rather than retrying — the safe default when you do not know why something was refused.

Transfers

CodeStatusMeaningWhat to do
PARTNER_TRANSFERS_DISABLED503Outbound transfers are not enabled in this environment yet.Contact us. Retrying will not clear it.
KILL_SWITCH_ENGAGED503All partner value movement is halted.Retry later; contact us if it persists.
TRANSFER_AMOUNT_INVALID422The amount is zero or not a positive KES amount with at most two decimals.Send a positive amount.
TRANSFER_DESTINATION_INVALID422The destination does not fit the rail, or the bank could not resolve it (details.field / details.providerCode).Fix the named field; do not retry the same destination unchanged.
TRANSFER_NAME_MISMATCH422The account holder's name we resolved does not match accountName (details.resolvedName).Check the name with your payee; retry with the resolved name only if you are sure.
TRANSFER_EXCEEDS_PER_ORDER_CAP422The transfer is larger than your per-instruction ceiling.Split it.
TRANSFER_EXCEEDS_DAILY_CAP422The transfer would breach your rolling 24-hour ceiling (orders and transfers together).Wait for the window to roll, or talk to us.
TRANSFER_INSUFFICIENT_BALANCE422Your KES account does not hold amount plus fee.Fund the account and retry.
TRANSFER_IDEMPOTENCY_CONFLICT409This idempotencyKey was used for a different request.Use a new key; the original transfer stands.
TRANSFER_NOT_FOUND404No transfer with that id belongs to you.Check the id.
TRANSFER_NOT_CONFIRMABLE409The transfer is no longer awaiting confirmation (details.status).Read the transfer; it has moved on.

Batch transfers

A batch's own per-item outcomes reuse every code in the table above (TRANSFER_NAME_MISMATCH, TRANSFER_EXCEEDS_DAILY_CAP, …) in each item's own failureCode — those are not restated here. Only the batch-level facts are new.

CodeStatusMeaningWhat to do
PARTNER_BATCH_TRANSFERS_DISABLED503Batch transfers are not enabled in this environment yet.Contact us. Retrying will not clear it.
BATCH_TOO_LARGE422The batch carries more items than the declared per-request limit.Split it into smaller batches.
BATCH_NO_VALID_ITEMS422Every item in the batch failed per-item validation. details.items gives each item's idempotencyKey and failureCode.Read details.items, fix them and resubmit; no code was issued.
BATCH_NOT_FOUND404No batch with that id belongs to you.Check the id.
BATCH_NOT_CONFIRMABLE409The batch is no longer awaiting confirmation (details.status).Read the batch; it has moved on.

USDT sends

CodeStatusMeaningWhat to do
PARTNER_USDT_SENDS_DISABLED503Outbound USDT sends are not enabled in this environment yet.Contact us. Retrying will not clear it.
USDT_SEND_DESTINATION_NOT_ALLOWLISTED422The destination is not a registered, active wallet of yours. Also returned for a wallet belonging to another partner — the two are indistinguishable on purpose.Register the destination with POST /v1/wallets and confirm it, then retry.
USDT_SEND_BELOW_MINIMUM422The amount is below our minimum for a USDT send.Send a larger amount.
USDT_SEND_AMOUNT_PRECISION422The amount needs more than 2 decimal places, which we cannot represent for this currency.Round to at most 2 decimal places and retry.
USDT_SEND_EXCEEDS_PER_ORDER_CAP422The amount alone, in KES-equivalent at the current mid-rate, is above your per-instruction ceiling.Split it.
USDT_SEND_EXCEEDS_DAILY_CAP422It would breach your rolling 24-hour KES-equivalent ceiling (orders, transfers and sends together).Wait for the window to roll, or talk to us.
USDT_SEND_INSUFFICIENT_LEDGER_BALANCE422Your USDT ledger balance does not cover the send (onlink_managed wallets only).Fund the wallet and retry with a new send.
USDT_SEND_RATE_UNAVAILABLE503The current USD:KES mid-rate needed to check your caps could not be read.Retry shortly.
USDT_SEND_NOT_CONFIRMABLE409The send is no longer awaiting confirmation (details.status).Read the send; it has moved on.
USDT_SEND_NOT_FOUND404No send with that id belongs to you.Check the id.

USDT_SEND_DESTINATION_NOT_ALLOWLISTED follows the money-movement exception in our own conventions: the destination wallet is the thing being verified, so this code deliberately does not distinguish "does not exist" from "belongs to someone else" from "not yet active" — the same opacity TRANSFER_NAME_MISMATCH's sibling codes do not need, because a wallet is a machine identifier and a bank account name is not.

A send that fails after confirmation carries its reason in failureReason on GET /v1/usdt/sends/:id, not as an HTTP error — see Send USDT from your wallet for the full failure model and why some failures reverse a ledger debit while others deliberately do not.

Collections

CodeStatusMeaningWhat to do
PARTNER_COLLECTIONS_DISABLED503Collections are not enabled in this environment yet.Contact us. Retrying will not clear it.
COLLECTIONS_WALLET_NOT_PROVISIONED503Your account is not yet fully set up to accept collections.Contact us; nothing about your request caused this.
BRIDGE_UNAVAILABLE503We could not open your deposit account right now.Retry shortly; contact us if it persists.
COLLECTION_VIRTUAL_ACCOUNT_PROVISION_IN_FLIGHT409A create for this exact collection is already being processed.Wait briefly, then read the collection rather than creating another.
COLLECTION_VIRTUAL_ACCOUNT_RECONCILIATION_REQUIRED409We could not confirm this collection's deposit account was opened only once.Contact us with the requestId; do not retry this request.
COLLECTION_STATIC_MEMO_PROVISION_IN_FLIGHT409A create for this exact collection is already being processed.Wait briefly, then read the collection rather than creating another.
COLLECTION_STATIC_MEMO_RECONCILIATION_REQUIRED409We could not confirm this collection's deposit reference was opened only once.Contact us with the requestId; do not retry this request.
COLLECTION_CURRENCY_UNSUPPORTED422Only USD is supported today.Send currency: "USD".
COLLECTION_NOT_FOUND404No collection with that id belongs to you.Check the id.

expectedAmount on POST /v1/collections is advisory only — a mismatch against the real deposit is reported later (never as an error on this route).

Confirmations

CodeStatusMeaningWhat to do
CONFIRMATION_NOT_FOUND404No confirmation with that id belongs to you.Check the id from the 202 response.
CONFIRMATION_CODE_INVALID422The code is wrong (details.attemptsRemaining).Ask your registered person for the code again; do not guess.
CONFIRMATION_LOCKED403Too many wrong codes (details.retryAfterSeconds).Wait, then request a new code. Contact us to have the lock cleared sooner.
CONFIRMATION_EXPIRED409The code's 5-minute window passed.Request a new code with /resend.
CONFIRMATION_ALREADY_VERIFIED409This confirmation was already accepted.Read the transfer; nothing more to confirm.
CONFIRMATION_CANCELLED409An operator cancelled the instruction before it was confirmed.Create a new transfer if still wanted.
CONFIRMATION_RESEND_TOO_SOON429The resend ladder has not elapsed (retryAfterSeconds, Retry-After).Wait the stated seconds.
CONFIRMATION_CONTACT_MISSING503Your partner account has no confirmation contact registered.Ask us to set one; nothing can be confirmed until then.

Confirmation codes carry extra context

CONFIRMATION_CODE_INVALID carries details.attemptsRemaining; CONFIRMATION_LOCKED and CONFIRMATION_RESEND_TOO_SOON carry a wait in seconds.

What is worth retrying

  • 429 — yes, after the interval in the Retry-After-partner header.
  • 5xx and network timeouts — yes, with backoff, and reuse the same partnerReference. That is what makes a retry safe: the same reference returns the existing order rather than creating a second.
  • Everything else — no. A 4xx means the request will be refused the same way until something changes.

On this page