TchokoPay
Guides

Errors & rate limits

Every error you'll see, how fast you can call, and the field naming rules.

Field naming

Three rules the whole API follows, so you never have to check which endpoint you're on:

referenceAlways ours. The identifier we generate — REQ-… for a collection, PO-… for a disbursement.
merchantReferenceAlways yours. Send it under this name, read it back under this name.
phoneThe mobile money number, whether you're collecting from it or paying it.
providerWhich network — mtn_cm, orange_cm. Same name on every request and response.

reference used to be accepted as the name for your identifier on requests, and still is. But it meant one thing going out and another coming back, so merchantReference is the name to use.

Error format

Every error has the same shape:

{
  "code": "INSUFFICIENT_BALANCE",
  "message": "Insufficient balance. You have 100 XAF, this needs 5250 XAF.",
  "param": "amount",
  "details": { "currency": "XAF", "available": 100, "required": 5250 },
  "requestId": "8e281c99-05b6-4fc3-ba79-58c55198b40b"
}
FieldDescription
codeBranch on this. Stable, and it will not change when somebody rewords a sentence.
messageWritten to be shown to a person as-is. Never a contract — treat the wording as free to change.
paramThe field at fault, when the failure is about one. Absent otherwise.
detailsThe numbers behind the message, so you never have to parse the sentence. Present on balance and amount failures. Absent otherwise.
requestIdAlso returned as the X-Request-Id header. Quote it to support and they can find the exact call.

Never branch on message. It is written for humans and gets improved. code is the contract.

Codes

codeMeaning
Access
UNAUTHORIZEDKey missing, invalid, or revoked.
MERCHANT_NOT_APPROVEDYour account is not in a state that can transact.
INSUFFICIENT_SCOPEThe key is real but was not issued this scope. Generate one that has it.
CAPABILITY_DISABLEDSwitched off for everybody, not for you. Nothing to fix on your side.
PROVIDER_BLOCKEDYour account is restricted from that network.
Request
INVALID_REQUESTFailed validation. param names the field.
IDEMPOTENCY_KEY_REQUIREDPayouts need one. Send a unique Idempotency-Key.
IDEMPOTENCY_KEY_TOO_LONG200 characters or fewer.
Currency, country, network
UNKNOWN_CURRENCYNot a currency we support. See GET /v1/limits.
CURRENCY_MISMATCHThe currency is not the one that country uses. details carries expected.
UNKNOWN_PROVIDERNot a provider you can use. See GET /v1/account/providers.
PROVIDER_COUNTRY_MISMATCHThat network does not operate in that country.
COUNTRY_NOT_SUPPORTEDWe cannot place mobile numbers there.
PHONE_NOT_RECOGNISEDNot a mobile money number we recognise in that country.
PHONE_PROVIDER_MISMATCHA number we can place, but not on the network you named.
Amount and balance
AMOUNT_TOO_SMALLBelow the currency minimum. details.minimum.
AMOUNT_TOO_LARGEAbove the currency maximum. details.maximum.
AMOUNT_PRECISIONMore decimal places than the currency has.
INSUFFICIENT_BALANCENot enough in that wallet. details.available and details.required.
PAYOUT_LIMIT_EXCEEDEDPast a per-payout, daily or weekly ceiling. details carries the ceiling.
PAYOUTS_NOT_CONFIGUREDPayouts are stopped for that currency — either on your account, or because the currency isn't open at all. Call disburse-limits and read blockedBy to see which. Not something you can cause with a bad request.
Flow
NOT_FOUNDNo such reference, or it belongs to another account. Identical either way, so you cannot use it to probe.
RECIPIENT_BUSYThat number has had enough payment prompts for now. Wait and retry.
RATE_LIMITEDToo many requests. See below.
SERVER_ERROROurs. Retry, and quote the requestId if it persists.

CAPABILITY_DISABLED and PAYOUTS_NOT_CONFIGURED are not your bug. Neither is caused by anything in your request, and neither goes away on retry. Both mean a switch is off on our side. Check GET /v1/account/disburse-limits/{currency} — its enabled field is the single answer to "can I disburse right now", and blockedBy names what to ask support for.

A currency is tied to its country. XOF with a Cameroonian network is CURRENCY_MISMATCH, not a conversion. XAF and XOF are both called the CFA franc and are pegged to the euro at the same rate, but they are different money and each has its own balance.

Only one error is reported per request — the first thing wrong, so you have one thing to fix rather than a list to work through.

Status codes

StatusMeaning
400Validation error — bad amount, unknown currency, more decimal precision than the currency supports, Idempotency-Key too long, malformed webhook URL.
401Missing, invalid, or revoked API key.
403Your merchant account isn't approved, your key lacks the scope, or the capability is switched off platform-wide. code says which.
404The payment reference doesn't exist, or belongs to a different merchant account — both look identical, so you can't use this endpoint to probe whether a reference exists.
429Rate limit hit — see below.

Rate limits

Every key starts with a default ceiling:

30 requests / minute

Short burst window.

600 requests / hour

Sustained volume window.

Exceeding either returns 429, naming the window and the limit:

{
  "code": "RATE_LIMITED",
  "message": "Too many requests in 1 minute (limit 30) — please slow down.",
  "requestId": "..."
}

If the default is too small

It is sized for an ordinary integration — a checkout creating a payment and polling it. If you're driving real volume, the ceiling is raised per key: talk to us at tech@tchokopay.com and tell us roughly what throughput you expect. A raised limit takes effect immediately, with no key rotation.

Limits are per key, not per account. Your server-side integration can be given headroom your public checkout key does not need. Two integrations sharing one key share one ceiling, so give each its own.

On this page