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"
}

Three fields are appended when they apply:

  • errorCode — present on some refusals, absent on most. On a 403 insufficient_scope it names the scope your credential is missing.
  • details — structured context beside a short code, on 4xx refusals only: details.attemptsRemaining, details.status, details.field and the like. The catalogue below names each one where it exists.
  • 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. Not every message is a short code: a validation failure reads Validation failed on: <fields>, a path id that is not a UUID reads Validation failed (uuid is expected), and a handful of refusals on quotes, orders, wallets and approvers read as a sentence — listed under Refusals that read as sentences. Match those verbatim rather than assuming every message is an upper-snake-case short code.

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 failed validation — the body, a query parameter, or a path id that is not a UUID.No. Fix the request.
401Authentication failed. Uniform, with no reason given.No. See below.
403Your credential is authenticated but not authorised for this endpoint, or a code is locked out.No. Ask us for the scope.
404No such route, or no such order, transfer, send, collection or wallet for your partner account.No.
409A conflict with existing state — a spent quote, a reused reference, a hash already attached.Only after changing something.
413The request body is larger than we accept.No. Send a smaller body.
422The request breaches one of your caps, or your balance cannot cover it.No. Fix the amount, or fund the balance, then try again.
429Rate limited, or a confirmation code was asked for again too soon.Yes, after the wait the response names.
500Something went wrong while handling the request. The message is deliberately generic.Yes, with backoff and the same partnerReference. Quote requestId.
502A service we depend on answered with an error.Yes, with backoff.
503Your account readiness, the settlement account, or a platform-wide control is not ready right now.Depends on the code. See below.
504A service we depend on did not answer in time.Yes, with backoff. Check the order or transfer before creating again.

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
quoteId not found.404No quote with that id belongs to you. This one is a sentence rather than a stable short code — match it exactly as shown, trailing period included. Usually a quoteId copied wrong, or one belonging to a different partner account. An expired quote is QUOTE_EXPIRED_OR_CONSUMED, not this.No. Take a fresh quote.
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.
USDT_SEND_BELOW_MINIMUM422POST /v1/quotes for a buy delivered to your own wallet came to less than 5 USDT, the least we can send on-chain. A buy credited to your managed USDT balance has no minimum. The same code is returned again at order create and by a USDT send.No, not at this size. Quote at least 5.00 USDT.
USDT_SEND_AMOUNT_PRECISION422POST /v1/quotes for a buy delivered to your own wallet named a usdtAmount with more than 2 decimal places, which we cannot send on-chain. A quote named by kesAmount never trips this: its USDT is rounded down to 2 places for you.No. Quote with at most 2 decimal places, or use kesAmount.

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_SIDE409No current route returns this code; it is reserved. GET /v1/orders/{id} returns either side's order.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 a sell order that is no longer awaiting_usdt — including one paid from your USDT balance, which leaves that status when it is created.No. Read the order; it has already moved on. See PATCH /v1/orders/{id}.
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 is not active, so it cannot create orders, transfers, USDT sends or collections. A suspension, not a problem with this request.No. Contact us; retrying will not clear it.

Account provisioning

Your KES balance account — the one you fund with M-PESA or a bank transfer (see Receive money in Kenya), and the one POST /v1/orders/buy debits directly — 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 balance 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 balance 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_DISABLED503USDT/KES orders are not enabled for your account right now.Contact us. Nothing about your request is wrong, and retrying will not clear it until the feature is enabled.
MONEY_MOVEMENT_PAUSED503All USDT/KES value movement is halted as an incident control.Do not retry in a tight loop. Wait for us to confirm the incident has cleared.
USDT_SEND_BELOW_MINIMUM422The USDT amount is below 5 USDT, the least we can send on-chain. Only for buys delivered to your own wallet; a buy credited to your managed USDT balance has no minimum. Refused when you take the quote, and again before your quote is consumed and before any KES is debited. It is the code a USDT send returns for the same limit.No, not at this size. Take a new quote for at least 5.00 USDT. The unused quote is untouched.
USDT_SEND_AMOUNT_PRECISION422The usdtAmount you named needs more than 2 decimal places, which we cannot send on-chain. Only for buys delivered to your own wallet; a buy credited to your managed USDT balance keeps all 6 places. Refused when you take the quote, and again before your quote is consumed and before any KES is debited. A quote named by kesAmount never trips this: its USDT is rounded down to 2 places for you.No. Quote with usdtAmount at no more than 2 decimal places (for example "25.50"), or name the amount with kesAmount.
TREASURY_ACCOUNT_NOT_REGISTERED503No settlement account is registered for this currency at all.Contact us with the requestId. Retrying will not clear it.
TREASURY_ACCOUNT_UNAVAILABLE503The settlement account could not take the debit. Usually this is before anything happens: your quote is not consumed and no order exists. Rarely it is our final re-check just before the debit: the order was created and rejected, the quote is spent, and nothing was debited.Retry once with the same quote and partnerReference. If the retry returns 202 with status: "rejected", it was the late case: take a fresh quote and use a new partnerReference. If it refuses again, stop and contact us with the requestId — it will not clear on its own.
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 quote's kesTotalAmount (principal plus fee).Fund your KES balance, then retry — this refusal happens before your quote is consumed.
KES_DEBIT_FAILED503We did not debit you: the bank cleanly refused the debit, or we stopped before sending it. The order is normally rejected — read it to confirm.Read the order to confirm, then create a new order against a fresh quote with a new partnerReference — reusing the old one returns the same rejected order, not a fresh attempt. 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 create a new order. The response carries no orderId: re-send the same request with the same partnerReference, which returns the existing order without a second debit, or wait for the order's webhook.

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 bank cleanly refused the debit, or we stopped before sending it. 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 have been refused after the bank confirmed your debit, and then your KES was taken and is owed back. Read kesDebit.status on the order — never assume from the word "rejected" alone: failed means we hold no record that your KES was taken (contact us with the orderId if it did leave your balance), reversal_pending means it was taken and is owed back, reversed means it has been returned. See Buy USDT with KES.

Sell leg (settlement account check)

POST /v1/orders/sell checks the settlement account before it consumes your quote, so these refusals leave the quote usable — except a late TREASURY_ACCOUNT_UNAVAILABLE, described in its row below.

CodeStatusMeaningWhat to do
TREASURY_ACCOUNT_NOT_REGISTERED503No settlement account is registered for this currency at all.Contact us with the requestId. Retrying will not clear it.
TREASURY_ACCOUNT_UNAVAILABLE503Raised before your quote is consumed, or — when paying from your USDT balance — at our final re-check, in which case the order is rejected and nothing was debited.Retry once with the same partnerReference; if it returns a rejected order, take a fresh quote with a new reference.

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_DISABLED503Selling from your own wallet is not enabled for your account. Checked before the order is created, any wallet call and any debit: no order was created and your quote was not used.Contact us. Once it is enabled, retry with the same partnerReference and quote. Nothing about your request is wrong, and nothing was collected.
USDT_COLLECTION_FAILED503The collection was refused, or never reached the provider, so nothing was collected from your wallet. The order is now rejected.Create a new order against a fresh quote with a new partnerReference. Do not retry this request.
USDT_COLLECTION_OUTCOME_UNKNOWN503We could not confirm whether the USDT left your wallet. The order is held for us to reconcile; it is not rejected.Do not create another order or resend USDT. Read the order with GET /v1/orders/{orderId} — re-sending the same request with the same partnerReference returns it — and contact us with the requestId.
WALLET_OWNERSHIP_CHECK_FAILED503The wallet named on the order did not pass our ownership check, so no collection was attempted. Your quote is spent and the order exists in awaiting_usdt until it expires.Stop retrying and contact us with the requestId — this does not clear on its own. To try again, take a fresh quote and use a new partnerReference; reusing the old one returns the stranded order.
USDT_RAIL_NOT_CONFIGURED503Our own custody configuration for this rail is incomplete. Entirely ours, and nothing about your request caused it. Your quote is spent and the order exists in awaiting_usdt until it expires.Contact us with the requestId. Retrying will not clear it. To try again, take a fresh quote and use a new partnerReference; reusing the old one returns the stranded order.

A refused collection and an unconfirmed one are different answers

USDT_COLLECTION_FAILED means the collection was refused or never left our side, so nothing was collected and the order is rejected. When the request timed out or the connection dropped mid-flight we cannot say which way it went, so we do not reject: the order is held in review and the answer is USDT_COLLECTION_OUTCOME_UNKNOWN. A held order sends no webhook until we have reconciled it, and it does not expire on its own. Do not create another order or resend USDT while it is held.

Sell leg (USDT balance debit)

When your USDT is held as a balance with us, POST /v1/orders/sell debits that balance at create instead of collecting from a wallet. These codes cover the ways that debit can be refused. The first is a clean refusal; the other four are failures to record the debit, and they do not move the order out of awaiting_usdt.

CodeStatusMeaningWhat to do
INSUFFICIENT_USDT_BALANCE422Your available USDT balance is below the order amount. Nothing was debited. The order is now rejected, and one order.rejected webhook is sent for it.Top up your USDT balance (the depositAddress on a sell response is yours to fund), then create a new order against a fresh quote with a new partnerReference. Do not retry this request.
LEDGER_POST_FAILED503We could not record the debit against your USDT balance — a database failure or timeout while recording it. We cannot say the debit did not land. The order is created and stays awaiting_usdt.Do not create a second order. Read the order via GET /v1/orders/{orderId}; retrying with the same partnerReference returns that order. Contact us with the requestId if it does not move on.
LEDGER_SUBJECT_CONFLICT409A balance entry already exists for this order and does not match this debit. Not something your request can cause.No. Contact us with the requestId.
LEDGER_POST_REJECTED422The balance entry for this debit was refused by our own integrity checks. Ours to look into; nothing about the amount you sent is known to be wrong.No. Contact us with the requestId; retrying the same request is refused the same way.
LEDGER_NOT_APPLICABLE409Your account does not hold a USDT balance with us, so there is nothing to debit. Reached only if the account's wallet arrangement changed after the order was created.No. Contact us with the requestId.

An insufficient balance is the only one of these five that is the partner's to fix. For the other four, read the order before doing anything else: a LEDGER_POST_FAILED in particular does not tell you whether the debit landed.

Deposits and payouts

CodeStatusWhat it meansWhat to do
TX_HASH_ALREADY_ATTACHED409This order already has a different hash. Re-sending the same one is a 200, not this.No. One hash per order; it is never overwritten.
TX_HASH_ALREADY_USED409That hash is already on another of your orders. 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 right now — frozen, closed or not yet active — or the id is one you saved before 2026-10-30 that is not your own OnLink KES account (for example one with a bank code).No. If your OnLink KES account cannot take a credit right now, contact us. If you saved the id earlier, read GET /v1/payout-accounts again and use an id from it.
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.

403 is not one thing

A 403 from the credential checks carries no code from this catalogue, and it means one of two different refusals — read error to tell them apart. In both, message is a sentence rather than a short code:

  • insufficient_scope — your credential is authenticated but was not granted the scope this route needs. errorCode names the missing scope, and message repeats it (This credential is not authorised for this endpoint. Required scope: orders:write.), so the refusal tells you what to ask for. See Products and scopes.
  • product_not_supported — your partner account's product does not offer this route at all, whatever scopes your credential holds. This deliberately does not enumerate which products the route does accept — that would describe another product's API surface to a credential that holds none of it. Ask us rather than requesting a scope; no scope grant fixes this one.

Two other routes return a 403 of their own, and neither is a credential refusal: CONFIRMATION_LOCKED (see Confirmations) and the wallet-approval refusals under Refusals that read as sentences.

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 for your account.Contact us. Retrying will not clear it.
MONEY_MOVEMENT_PAUSED503All partner value movement is halted.Retry later; contact us if it persists.
TRANSFER_AMOUNT_INVALID422The amount is zero or not a positive amount with at most two decimals.Send a positive amount.
TRANSFER_CURRENCY_NOT_ENABLED422Your account cannot send this currency on any rail: USD payouts are for Collections accounts, and a USDT Rails account sends KES only (details.currency, details.enabledCurrencies). Also returned by a batch, for the whole batch. Nothing was written.Send KES, or contact us about a Collections account.
TRANSFER_RAIL_CURRENCY_UNSUPPORTED422The rail cannot pay this currency: USD over mpesa, pesalink or internal, or a USD SWIFT payout to an account in another currency (details.rail, details.currency, details.destinationCurrency).Send USD over rtgs, eft or swift to a USD account, or send KES.
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 (a USD transfer at its KES equivalent).Split it.
TRANSFER_EXCEEDS_DAILY_CAP422The transfer would breach your rolling 24-hour ceiling (orders, transfers and USDT sends together; USD transfers at their KES equivalent).Wait for the window to roll, or talk to us.
TRANSFER_INSUFFICIENT_BALANCE422The account the transfer pays from (KES, or USD for a USD transfer) does not hold amount plus fee.Fund the account and retry.
TRANSFER_DAILY_CAP_RATE_UNAVAILABLE503The cap check needed a live market rate (to count a USD transfer, or your committed USDT sends, in KES) and none was available. Also returned by a batch.Retry shortly. Nothing about your request is wrong.
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 for your account.Contact us. Retrying will not clear it.
MONEY_MOVEMENT_PAUSED503All partner value movement is halted.Retry later; contact us if it persists.
BATCH_TOO_LARGE422The batch carries more items than the declared per-request limit.Split it into smaller batches.
BATCH_CURRENCY_MIXED422The items send more than one currency (details.currencies). A batch pays from one account under one code.Submit one batch per currency; nothing was written.
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 for your account.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_DESTINATION_BLOCKED409This destination cannot be used. The response gives no reason.Contact us. Retrying will not clear it.
USDT_SEND_DESTINATION_SCREENING_UNAVAILABLE503We could not check the destination just now. Nothing was created.Retry the same request shortly.
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_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.

An insufficient balance is NOT a create-time refusal

Unlike the buy and sell legs, a send's onlink_managed-wallet ledger balance is checked at execute time, after your confirmation — never at create. A shortfall there does not raise an HTTP error; it ends the send failed with USDT_SEND_INSUFFICIENT_LEDGER_BALANCE in failureReason. Fund the wallet and start a new send.

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.

Two of those reasons concern the destination, and they ask different things of you. DESTINATION_BLOCKED means the destination can no longer be used: contact us; retrying will not clear it. DESTINATION_SCREENING_UNAVAILABLE means we could not finish checking the destination when the send ran: nothing was debited or sent, so retry later by creating the send again.

USDT holding wallet

CodeStatusMeaningWhat to do
HOLDING_WALLET_NOT_ALLOCATED503No holding wallet address has been allocated for your account yet, so there is no address to return.Contact us. It is not self-serve, and retrying will not clear it until we have set it up. Nothing was affected.

Returned by GET /v1/usdt/holding-wallet. The response never carries an empty address, so an integration cannot send USDT to nowhere. errorCode carries the code, and message starts with it, followed by the next step.

Collections

CodeStatusMeaningWhat to do
PARTNER_COLLECTIONS_DISABLED503Collections is not enabled for your account.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.
COLLECTIONS_RAIL_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 safely reuse or create the deposit instructions for this collection.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 safely reuse or create the deposit instructions for this collection.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.
COLLECTION_NOT_OPEN409The collection is no longer open; details.status names its status. Reserved for editing, cancelling and sharing a collection, which are not available on /v1 yet; no /v1 route returns it today.Read the collection; do not retry.
NOTHING_TO_UPDATE400An edit named no field to change. Reserved for editing a collection, which is not available on /v1 yet; no /v1 route returns it today.Send at least one field to change.
INVALID_IDEMPOTENCY_KEY400The Idempotency-Key header is not 8 to 64 characters of A-Z, a-z, 0-9, _ and -. Nothing was created.Send a valid key, or omit the header.
IDEMPOTENCY_KEY_REUSED422That idempotency key was already used with different details, or the collection it created has been edited since (the key is compared with the collection as it stands now). Carries no details.Retry only the identical request while the collection is unedited; otherwise read the collection, and use a new key for a new create.

expectedAmount on POST /v1/collections is advisory only — it is stored and echoed back when you read the collection, and that is all it does. Nothing on this API ever compares it against the real deposit, so a mismatch produces no error, no event and no field telling you it happened.

Conversions

Every refusal below is returned before anything is debited: a refused POST /v1/conversions moved no money and created no conversion.

CodeStatusMeaningWhat to do
PARTNER_CONVERSIONS_DISABLED503Conversions are not enabled right now.Contact us. Retrying will not clear it.
CONVERSION_PAIR_UNSUPPORTED422Only from: "USD" to to: "KES" is supported.Send USD and KES.
CONVERSION_AMOUNT_INVALID422amount is not a positive decimal string with at most 2 decimal places.Send an amount such as "100.00".
CONVERSION_AMOUNT_BELOW_MINIMUM422amount is below USD 1.00.Convert at least USD 1.00.
CONVERSION_AMOUNT_ABOVE_MAXIMUM422amount is above the maximum for one conversion, which is the same for every partner.Split it into several conversions, or contact us for the current maximum.
CONVERSION_INSUFFICIENT_BALANCE422Your USD account holds less than amount.Convert a smaller amount, or wait for more USD to land.
CONVERSION_RATE_UNAVAILABLE503No exchange rate could be read right now.Retry shortly; contact us if it persists.
CONVERSION_BALANCE_UNAVAILABLE503Your USD balance could not be read right now.Retry shortly with the same idempotencyKey.
CONVERSIONS_UNAVAILABLE503Conversions are temporarily unavailable. Nothing about your request is wrong, and nothing moved.Retry shortly with the same idempotencyKey; contact us with the requestId if it persists.
MONEY_MOVEMENT_PAUSED503All partner value movement is halted.Retry later; contact us if it persists.
IDEMPOTENCY_KEY_REUSED422That idempotencyKey was already used for a conversion with different details.Retry only the identical request; use a new key for a new conversion.
CONVERSION_NOT_FOUND404No conversion with that id belongs to you.Check the id.

Sandbox simulations

These come from the two sandbox-only routes that act as if money arrived, POST /v1/simulations/collections/{id}/deposits and POST /v1/simulations/orders/{id}/usdt-receipt. Outside the sandbox those routes answer a plain 404, exactly like a path that does not exist, so you never see these codes there.

CodeStatusMeaningWhat to do
SIMULATION_AMOUNT_INVALID400The amount is not a positive decimal with the number of places the route allows. You normally meet this refusal first as Validation failed on: amount; this is the same rule.No. Send a decimal string: at most 2 places and no more than 10000.00 for a collection, at most 6 for USDT.
COLLECTION_NOT_OPEN409The collection is not open, so there is nothing for a payment to settle. Usually it was already paid, or it expired.No. Read the collection to see its status, or open a new one.
COLLECTION_NOT_PROVISIONED409The collection has no deposit instructions yet, so a payment has nowhere to land.Not yet. Read the collection until its deposit instructions appear, then retry. Contact us if they never do.
ORDER_NOT_FOUND404No order with that id belongs to you.No. Check the id.
ORDER_TX_HASH_NOT_ATTACHED409The sell order has no transaction hash attached, so a deposit has nothing to match.Not yet. Attach the hash with PATCH /v1/orders/{id}, then retry.
ORDER_USDT_COLLECTED_AUTOMATICALLY409The USDT for this order is collected from your customer wallet for you, so there is no deposit to simulate.No. Read the order; it moves on without a simulated deposit.
SIMULATION_NOT_CONFIGURED503The simulation cannot run for this object right now. For a collection, the usual cause is that, while sandbox crediting is enabled, it was created before sandbox money was simulated; otherwise it is ours to fix.For a collection: create a new one and simulate that. Otherwise yes, after we fix it; contact us with the requestId.
SIMULATION_UPSTREAM_UNAVAILABLE503We could not complete the simulated payment, and nothing is known to have happened.Yes. Retry shortly. For a USDT deposit, repeat the amount you sent the first time.

ORDER_NOT_SELL_SIDE and ORDER_NOT_AWAITING_USDT (see Orders) also come from the USDT deposit route, with the same meaning: the order is a buy order, or it is no longer waiting for a deposit.

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_CANCELLED409The instruction was cancelled 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_DELIVERY_UNAVAILABLE503Your account receives confirmation codes by webhook, but no working webhook is configured. Nothing was created.Contact OnLink support. See Auto-confirm instructions.

Confirmation codes carry extra context

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

A waived account may get no confirmation at all

When OnLink has enabled confirmation waiving for your account, a create can return status: "confirmed" with no confirmation object. There is nothing to confirm, and a confirm call against such a payout answers CONFIRMATION_ALREADY_VERIFIED. In production, a waived account whose API key has no IP allowlist is still asked for a code, and its confirmation block carries waiverNotApplied: "key_without_ip_allowlist" on the first response only (a repeat of the same request returns the stored payout without it); ask us to add an IP allowlist to that key. If the account also has no confirmation contact (or no working webhook), there is no code to send, so the create is refused CONFIRMATION_CONTACT_MISSING or CONFIRMATION_DELIVERY_UNAVAILABLE (503) and nothing is created. When the cause is the missing allowlist, that 503 carries details.waiverNotApplied: "key_without_ip_allowlist". See When confirmation is waived.

Withdrawal addresses

Returned by POST /v1/wallets/{id}/confirm when the address is yours and has already moved past the step this call performs, so there is nothing left to confirm. Both carry details.status (the address's current status) and details.nextStep (a sentence saying what to do).

CodeStatusMeaningWhat to do
WALLET_ALREADY_CONFIRMED409Your approver's confirmation was already recorded. details.status is pending_onlink_approval (awaiting OnLink approval, not yet usable for delivery) or active (already usable).No. Do not register the address again and do not confirm again. Read GET /v1/wallets to follow its status; only active can receive value.
WALLET_REVOKED409The address was revoked, which is terminal. details.status is revoked.No. Register a different address if you need one.

A code that is merely expired, cancelled or used up still answers 403, as does an id that is not one of your wallets (see Refusals that read as sentences); only your own wallets get the more specific 409.

Refusals that read as sentences

These come back in message as a sentence, not a short code, so match them verbatim or on their opening words. Where a value is shown in angle brackets, it is filled in with yours.

Quotes and orders

MessageStatusWhat it meansWhat to do
Provide exactly one of kesAmount or usdtAmount.400A quote takes one amount, not both and not neither.No. Send one.
<field> must be a positive decimal string with at most <n> decimal places.400An amount is not a decimal string, is negative, or has too many places.No. Send the amount as a string, e.g. "10000.00".
<field> must be greater than zero.400An amount is zero.No.
Amount is too small to quote at the current rate.400The amount rounds to nothing on the other side of the trade.No. Quote a larger amount.
partnerReference must not be empty.400An order was created with a blank partnerReference.No. Send your own reference.
expectedUsdtAmount must be a positive decimal string with at most 6 decimal places.400The optional assertion on a buy order is malformed.No. Pass the quote's usdtAmount verbatim, or omit it.
payoutAccountId not found.404No payout account with that id belongs to you.No. Read GET /v1/payout-accounts and pick one of those.
Order not found.404No order with that id belongs to you. Another partner's order reads exactly the same.No. Check the id.
partnerReference is already used by a buy order. / …by a sell order.409A reference names one order across both sides: it was already used on the other side.No. Use a fresh reference for this side.
A live rate is not available. Quotes are not issued on a degraded rate source.503We will not quote when we cannot stand behind the rate.Yes. Retry shortly.
A usable rate is not available right now.503The same, found while pricing.Yes. Retry shortly.
A USDT deposit address has not been allocated for your account yet.503Your account has no USDT deposit address yet. Also returned by GET /v1/funding, where the message begins with this sentence and carries more text after it.No. Contact us; retrying will not clear it.

Wallets and approvers

A withdrawal address is registered, confirmed by one of your approvers with a one-time code (an approver is registered with POST /v1/admins), then approved by us.

MessageStatusWhat it meansWhat to do
Unsupported chain '<chain>'. Only tron is supported.400Only Tron addresses can be registered today.No. Send chain: "tron".
Unsupported asset '<asset>'. Only USDT is supported.400Only USDT addresses can be registered today.No. Send asset: "USDT".
Address is not a valid tron address (base58check, checksum verified).400The address fails its checksum. We refuse here because a mistyped address is an unrecoverable loss.No. Copy the address again from its source.
Register an administrator (POST /v1/admins) before adding a withdrawal address — approval codes are sent to a registered administrator.409There is nobody to send the approval code to.Register an approver, then register the address.
Incorrect approval code.400The code does not match.Ask for the code again; do not guess.
Too many incorrect codes. This approval code has been cancelled.403The attempts for this code are used up.Register the same address again to have a new code.
This approval code is no longer usable., usually followed by Register the same address again to have a new code sent.403The code expired, was cancelled, or is for a wallet that is not one of yours. We do not say which on purpose. A wallet of yours that is already confirmed or revoked gets a 409 instead (see Withdrawal addresses).Register the same address again to have a new code.
Wallet not found.404No wallet with that id belongs to you.No. Check the id.
Administrator not found., from DELETE /v1/admins/{id}404No approver with that id belongs to you.No. Check the id.
Administrator email must not contain whitespace. / Administrator email is not a valid address. / Administrator email must be on the <domain> domain.400On POST /v1/admins: the email is malformed, or is not on your organisation's domain.No. Fix the address.

Anything else

MessageStatusWhat it meansWhat to do
Validation failed on: <fields>400The named body or query fields failed validation.No. Fix the named fields.
Validation failed (uuid is expected)400A path id is not a UUID.No. Check the id.
Request payload is too large413The body is larger than we accept.No. Send a smaller body.
Failed to record rejection for …503We could not record a refusal on an order we had already created.Yes. Read the order before creating another one.

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