Errors
Envelope
Every error response uses one shape:
{
"error": {
"code": "invalid_amount",
"message": "Amount must be a positive integer in minor units.",
"details": [
{ "code": "invalid_amount", "message": "Amount must be a positive integer in minor units." },
{ "code": "invalid_msisdn", "message": "MSISDN must be 8-15 digits in international format without '+'." }
]
}
}code— stable, machine-readable. Branch on this.message— English, for developers and logs. Not for end users, and subject to rewording.details— every error found, when a request has more than one problem.code/messageat the top level repeat the first entry, so simple clients can ignoredetailsentirely.
Validation reports all problems at once rather than one per round trip.
Localisation: branch on
codeand render your own message.messageis English-only and may be reworded without notice;codeis contractual and will not change without a/v2.
HTTP status codes
| Status | When |
|---|---|
200 | Successful read. |
201 | Resource created (collection, payout, refund). |
400 | Validation failure or malformed request. |
401 | Authentication problem. |
403 | Authenticated but not permitted. |
404 | No such resource for this merchant — see the note below. |
409 | Idempotency conflict or concurrent modification. |
5xx | Our fault. Safe to retry with the same idempotency key. |
On 404: another merchant's transaction returns 404, identical to a genuinely nonexistent id. This is deliberate — a 403 would confirm the id exists, letting an attacker enumerate other merchants' transactions. The same reasoning applies to checkout tokens, where unknown and malformed values answer identically.
Stable error codes
The complete catalogue. Codes not listed here are not part of the contract.
Authentication
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No Authorization header. |
invalid_api_key | 401 | Unknown, malformed, or revoked key. |
api_key_expired | 401 | The key was ours and has passed its expiry. Issue a replacement; keys live at most 180 days. Distinct from invalid_api_key because there is something the holder can do about it. |
environment_mismatch | 401 | Live key against sandbox URL, or vice versa. |
key_limit_reached | 400 | A third active key was requested for one environment; at most two are allowed. Revoke one, or let it expire. |
Idempotency
| Code | HTTP | Meaning |
|---|---|---|
idempotency_key_required | 400 | Header missing on a mutating request. |
idempotency_key_invalid | 400 | Header is not a UUID. |
idempotency_key_reuse | 409 | Key already used with a different body. |
idempotency_in_progress | 409 | Original request with this key still running. |
Request validation
| Code | HTTP | Meaning |
|---|---|---|
invalid_amount | 400 | Amount is not a positive integer in minor units. |
amount_exceeds_maximum | 400 | Amount is above the per-transaction ceiling (1014 minor units). A safety bound, not a commercial limit — nothing legitimate approaches it. |
invalid_currency | 400 | Unsupported currency code. |
invalid_msisdn | 400 | Not 8–15 digits in international format (no +). |
description_required | 400 | Description missing or blank. |
invalid_request | 400 | Malformed request with no more specific code. |
Payouts
| Code | HTTP | Meaning |
|---|---|---|
payout_exceeds_balance | 400 | The payout plus its fee exceeds your spendable balance in that currency. Message states the required and spendable amounts. |
Spendable is your posted balance minus payouts and refunds you have already requested that
have not settled yet — the ledger only posts on completion, so a second payout cannot spend
money the first one has already committed. Distinct from insufficient_float, which is
our problem, not yours.
Refunds
| Code | HTTP | Meaning |
|---|---|---|
invalid_collection_id | 400 | collectionId is not a valid id. |
not_a_collection | 400 | The referenced id is not a collection (col_…). |
not_refundable | 400 | The collection is not completed. |
refund_exceeds_refundable | 400 | Amount exceeds the remaining refundable balance. Message states the remainder. |
Hosted checkout
See checkout.md.
| Code | HTTP | Meaning |
|---|---|---|
invalid_return_url | 400 | returnUrl is not an absolute http(s) URL. |
checkout_already_paid | 400 | The session has been paid. One session yields at most one successful payment. |
checkout_expired | 400 | The payment link is past its 24-hour deadline. |
Transaction limits
Every merchant has a per-transaction ceiling and a daily total for each of collections, payouts and refunds. The figures are visible in the panel under Settings, and are set by MoniWave rather than by the merchant.
Only the per-transaction ceilings are enforced today. The daily totals are recorded and monitored but do not currently refuse a request.
| Code | HTTP | Meaning |
|---|---|---|
amount_exceeds_limit | 400 | Above this merchant's per-transaction ceiling for that operation. The message names the limit that was hit. |
amount_below_minimum | 400 | Below the minimum for a single transaction. |
currency_not_permitted | 400 | No limits are configured for that currency, so it cannot be transacted in. XAF is the launch currency. |
kind_not_permitted | 400 | No limits are configured for that operation. |
amount_exceeds_maximum is a different thing and stays where it is under Common: it is an
absolute arithmetic ceiling that protects the ledger, and it applies regardless of what any
merchant's limits say.
Webhooks
See webhooks.md.
| Code | HTTP | Meaning |
|---|---|---|
invalid_webhook_url | 400 | Not an absolute http(s) URL. |
webhook_url_not_public | 400 | The URL points at a private, loopback, link-local or internally-resolvable address. Webhook endpoints must be publicly reachable. |
webhook_secret_required | 400 | A signing secret is required. |
Transaction state
| Code | HTTP | Meaning |
|---|---|---|
invalid_transaction_id | 400 | Not a valid transaction id. |
already_terminal | 400 | Transaction already completed/failed/expired; terminal states never change. |
invalid_transition | 400 | The requested change is not legal from the current status. |
Concurrency
| Code | HTTP | Meaning |
|---|---|---|
EntityAlreadyExists | 409 | Resource already exists. |
ConcurrentError | 409 | Another writer modified the transaction concurrently. Re-read and retry. |
Provider failure reasons
Distinct from API errors: these describe why a transaction failed and appear in the failureReasonCode field of transaction responses and webhook payloads, not in an error envelope. A failed transaction is a successful API call.
| Reason code | Meaning |
|---|---|
payer_declined | The customer declined the prompt. |
insufficient_funds | The customer's wallet balance was too low. |
invalid_subscriber | Not a registered mobile-money subscriber. |
insufficient_float | Our float at the provider was too low to fund a payout or refund. Not the customer's fault; contact support. |
Retrying safely
4xxother than409 idempotency_in_progress— do not retry unchanged; fix the request.409 idempotency_in_progress— retry after a short delay with the same key.409 ConcurrentError— re-read the transaction and retry.5xx, timeouts, connection failures — retry with the same idempotency key. That is what makes retries safe rather than double-charging (idempotency.md).