Error codes
Every error is JSON in one envelope, with a message written for humans and a request_id you
can quote to support:
{ "error": { "type": "invalid_request", "code": "parameter_invalid", "message": "amount: Number must be greater than 0", "param": "amount", "request_id": "req_01J8XQ7M4K..." }}Types
type | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | The request itself is wrong — fix and resend |
authentication_error | 401 | Missing or invalid API key |
permission_error | 403 | Valid key, missing scope |
not_found | 404 | No such object for this merchant |
idempotency_conflict | 409 | Key reuse conflict — see below |
rate_limited | 429 | Slow down; retry after a pause |
api_error | 500 | Our fault. Safe to query, then retry |
Codes
| Code | Cause | Remedy |
|---|---|---|
parameter_invalid | A field failed validation; param names it | Fix the named field; the message says what’s wrong |
idempotency_key_required | POST /v1/payments without an Idempotency-Key header | Add the header — why it’s mandatory |
idempotency_key_reused | Same key, different body | You have two different requests sharing a key; give each intent its own key |
request_in_progress | Same key while the first attempt is still executing | Back off briefly and retry; you’ll get the original response replayed |
reference_in_use | This merchant already has a payment with this reference | Fetch the existing payment instead of creating a duplicate |
msisdn_invalid | Not a Zimbabwean mobile number | Normalise to 2637XXXXXXXX; show the error message to the user verbatim |
resource_missing | The id doesn’t exist, or belongs to another merchant | Check the id and which key (test vs live) you’re using — objects don’t cross environments |
link_expired | The payment link’s expires_at has passed | Create a fresh link |
link_used | A single-use link was already paid and its payment can’t be resolved | Create a fresh link; investigate the original payment in the dashboard |
refund_amount_invalid | Refund exceeds what remains refundable | Check prior partial refunds with GET /v1/refunds/{id} |
rate_limited | Too many requests | Exponential backoff; respect any Retry-After header |
internal | Something failed on our side | Query the payment before retrying — the charge may have succeeded — then retry with the same idempotency key |
The retry rule
For 5xx and network failures on a POST: query first, then retry with the same
Idempotency-Key. That order guarantees you never create a duplicate even when our answer
was lost on the wire. For 400-class errors, retrying without changing the request will never
help.