Errors
Errors use conventional HTTP status codes plus a machine-readable body: a closed catalog of error codes, a human sentence, and a next_step that tells you what to do about it.
Error shape
Validation failures list the offending fields in validation_errors. Every error carries the request_id to quote to support.
{
"status": "error",
"error": {
"type": "validation_error",
"code": "validation_error",
"message": "One or more request parameters are invalid.",
"next_step": "Fix the fields listed in validation_errors and retry the request.",
"validation_errors": [
{ "field": "country", "message": "country must be a 2-letter ISO code, e.g. CM" },
{ "field": "amount", "message": "amount is required as a decimal string, e.g. \"5000\"" }
],
"request_id": "c4a1f8e2-9d30-47b6-8e5f-1a2b3c4d5e6f"
}
}Error catalog
The catalog is closed: these are all the codes the API returns today. Retries with the same Idempotency-Key are always safe on 5xx responses.
| Code | HTTP | Type |
|---|---|---|
| invalid_api_key | 401 | authentication_error |
| insufficient_scope | 403 | authentication_error |
| route_not_found | 404 | invalid_request_error |
| resource_not_found | 404 | invalid_request_error |
| validation_error | 422 | validation_error |
| idempotency_key_reused | 422 | idempotency_error |
| request_in_flight | 409 | idempotency_error |
| rate_limited | 429 | rate_limit_error |
| ip_rate_limited | 429 | rate_limit_error |
| limit_exceeded | 403 | invalid_request_error |
| amount_below_minimum | 403 | invalid_request_error |
| insufficient_balance | 402 | invalid_request_error |
| merchant_restricted | 403 | invalid_request_error |
| settlement_managed | 403 | invalid_request_error |
| provider_unavailable | 503 | provider_error |
| unsupported_country | 422 | invalid_request_error |
| unsupported_currency | 422 | invalid_request_error |
| method_not_available | 422 | invalid_request_error |
| prohibited_corridor | 403 | invalid_request_error |
| corridor_closed | 503 | provider_error |
| corridor_awaiting_float | 503 | provider_error |
| service_paused | 503 | provider_error |
| card_data_rejected | 422 | invalid_request_error |
| risk_rejected | 403 | invalid_request_error |
| compliance_blocked | 403 | invalid_request_error |
| refund_unsupported_rail | 422 | invalid_request_error |
| refund_below_rail_minimum | 422 | invalid_request_error |
| refund_payer_details_missing | 422 | invalid_request_error |
| fx_pair_unsupported | 422 | invalid_request_error |
| fx_rate_unavailable | 503 | provider_error |
| sandbox_only | 403 | invalid_request_error |
| link_not_found | 404 | invalid_request_error |
| link_disabled | 410 | invalid_request_error |
| link_expired | 410 | invalid_request_error |
| report_window_invalid | 422 | validation_error |
| checkout_sessions_disabled | 403 | invalid_request_error |
| session_reference_in_use | 409 | invalid_request_error |
| session_not_open | 409 | invalid_request_error |
| session_not_found | 404 | invalid_request_error |
| session_expired | 410 | invalid_request_error |
| session_disabled | 410 | invalid_request_error |
| agent_live_money_disabled | 403 | invalid_request_error |
| agent_currency_not_allowed | 403 | invalid_request_error |
| agent_corridor_not_allowed | 403 | invalid_request_error |
| agent_beneficiary_not_allowed | 403 | invalid_request_error |
| agent_cap_per_transaction_exceeded | 403 | invalid_request_error |
| agent_cap_daily_exceeded | 403 | invalid_request_error |
| agent_cap_monthly_exceeded | 403 | invalid_request_error |
| agent_batch_not_allowed | 403 | invalid_request_error |
| agent_key_rotated | 403 | invalid_request_error |
| internal_error | 500 | api_error |
Reason codes
Errors describe a request that was refused; reason codes describe how an accepted transaction turned out. Every non-success outcome of a collection, payout or refund carries a reason_code from this closed vocabulary, on the resource itself, in its event history and in webhooks. Handle each code you care about explicitly, and make the default branch of your status switch mean undetermined: keep polling GET /{id} or wait for the webhook. A status your code does not handle is never a failure and never a success.
| Code | Category | Terminal | Description |
|---|---|---|---|
| payer_timeout | CUSTOMER | yes | The payer did not confirm the payment prompt in time. |
| insufficient_funds | CUSTOMER | yes | The payer's account balance was insufficient. |
| invalid_customer | CUSTOMER | yes | The customer identifier (MSISDN or account) is invalid or not registered. |
| cancelled_by_payer | CUSTOMER | yes | The payer explicitly declined or cancelled the payment. |
| provider_error | PROVIDER | yes | The provider returned an unrecoverable error. |
| provider_unavailable | PROVIDER | no | The provider is temporarily unavailable; the transaction may be retried or enqueued. |
| expired | PLATFORM | yes | The transaction expired before completion. |
| duplicate | PLATFORM | yes | A duplicate of an already-processed transaction. |
| unknown | PLATFORM | no | The outcome is not yet determined. |
| limit_exceeded | RISK | yes | A configured transaction or velocity limit was exceeded. |
| risk_review | RISK | no | The transaction is held for risk review. |
| amount_mismatch | RECONCILIATION | no | The provider-confirmed amount differs from the requested amount; under reconciliation. |
| insufficient_balance | PLATFORM | yes | errors.rc.insufficient_balance |
| resolve_unsupported | PLATFORM | yes | errors.rc.resolve_unsupported |
| batch_cancelled | PLATFORM | yes | errors.rc.batch_cancelled |
| awaiting_float | PROVIDER | no | The provider wallet funding this corridor is being replenished. The payout holds its reserve and sends as soon as the float is restored; nothing else is required of you. |