Errors & Status Codes
The API uses standard HTTP status codes. Error bodies are always JSON in the same shape.
Error body format
{
"success": false,
"data": null,
"error": "amount must be a multiple of the credit currency's unit step",
"code": "AMOUNT_NOT_DIVISIBLE",
"meta": {
"request_id": "019f2d23-d667-72f2-91de-747a79547402",
"timestamp": 1783168751
}
}Branch on code, not error — error is human prose for logs only. Keep meta.request_id (the same value as the X-Request-Id response header) to share with support.
Common status codes
| Status | Meaning | What to do |
|---|---|---|
| 200 / 201 | Success | Use the data field |
| 400 | Malformed request or validation failed | Check body/query per details |
| 401 | Authentication failed | Check API key / signature / timestamp |
| 403 | No permission | Check API key permissions in organization settings |
| 404 | Resource not found | Check id / path |
| 409 | State conflict, e.g. duplicate order_ref or idempotency reuse | Read code — RESOURCE_EXISTS: order_ref was already used (look up the original via GET ?order_ref=... before anything else); IDEMPOTENCY_REQUEST_IN_PROGRESS / OPERATION_IN_PROGRESS: wait and retry with the same Idempotency-Key; PAYIN_STATUS_TRANSITION_INVALID: status does not allow this action |
| 422 | Well-formed but rejected by business rules | Read error.code to handle specific cases |
| 429 | Rate limit | Read X-RateLimit-After / X-RateLimit-Remaining from the response headers, then retry with exponential backoff |
| 5xx | Server error | Retry with backoff; if it persists, contact support with the X-Request-Id response header |
order_ref — duplicate protection
For POSTs that create transactions (payins, payouts), order_ref must be unique per merchant and environment per transaction type, and a used value is never freed — even after the transaction fails, expires, or is cancelled. Reusing a value is rejected with 409 RESOURCE_EXISTS — the API does not return the existing transaction. If a retry is rejected as a duplicate, the original request already created the transaction — look it up with GET /v1/payins?order_ref=... (or GET /v1/payouts?order_ref=...) instead of creating a new reference. Settlements do not use order_ref — they use Idempotency-Key on POST /v1/settlements (optional client_operation_id).
Recommended retry policy
- Retry only 429, 502, 503, 504, and network errors
- On 429, read X-RateLimit-After from the response headers and wait before retrying
- Exponential backoff + jitter starting at ~250 ms, capped at ~8 s
- Limit max attempts to 3–5
- Do not retry non-429 4xx — fix the request instead. Exception: 409 IDEMPOTENCY_REQUEST_IN_PROGRESS or OPERATION_IN_PROGRESS — wait a few seconds and retry with the same Idempotency-Key
Getting help
- Missing webhook? Any HTTP 2xx is a successful delivery, while a 4xx is a permanent rejection and is not retried. Network errors, timeouts, and 5xx responses are retried with exponential backoff through the outbox, up to 20 delivery attempts including the first; there is no fixed 10-minute schedule. If the callback is still missing, check the status with GET /v1/payins/{id} / GET /v1/payouts/{id}. We recommend a reconcile job that polls still-open transactions periodically (e.g. every 1–5 minutes). For a payin in success status, authorized staff can queue a new payin.succeeded delivery from the payin detail page; the manual resend has a new webhook body id.
- Transaction problems (funds not credited, amount mismatch, duplicate QR): open a ticket with POST /v1/issues, sending issue_type, issue_sub_type and the request_id — there is no formal response SLA; the team picks tickets up in order. Track progress with GET /v1/issues and cancel an unclaimed ticket with PATCH /v1/issues/{request_id}/cancel.
- Integration problems that are not tied to a transaction (signature failures, webhook configuration, account opening): contact the support channel your organization received during onboarding, and include the X-Request-Id response header from the failing request.