Common Error Types
API Errors
HTTP status codes and their meanings:Business Logic Errors
Payment-specific decline scenarios and the codes that carry them:- Insufficient funds: your balance cannot cover the payout (
INSUFFICIENT_FUNDS) - Invalid recipient details: the beneficiary’s details are incomplete or invalid for the rail (
RECIPIENT_DETAILS_INVALID, or a400with adetailsarray at submit time) - Limits: a per-transaction, daily, or corridor limit would be exceeded (
LIMIT_EXCEEDED) - Payouts paused: payouts are not currently permitted for your workspace (
PAYOUT_NOT_PERMITTED) - Rail unavailable: the payout could not be completed on the selected rail right now (
PAYOUT_PROVIDER_UNAVAILABLE) - Compliance holds: a payout can fail after screening; held funds are returned to your balance automatically
Error Response Structure
Zuba is converging on RFC 9457 problem details. Errors arrive in one of three shapes, distinguishable by their fields.Problem details (primary)
Migrated endpoints respond withContent-Type: application/problem+json and a flat envelope (there is no nested error object):
Legacy shape
Endpoints not yet migrated return the legacy top-level shape:Validation shape
Field validation failures are400 responses with a details array. See Recipient field validation below for the full contract.
Basic Error Handler
One handler covers all three shapes. Read the top-level fields and fall back for legacy bodies:code; treat a missing code as unknown and fall back to the HTTP status.
Error Code Registry
code values are stable and never change meaning. Each maps to a failure category that fixes the HTTP status and retry semantics:
The “Retryable” column shows the registry default. The envelope’s
retryable
flag is authoritative for each response: on POST requests it is forced to
false unless the request carries an Idempotency-Key, so a retry can never
double a payout. Always branch on the flag, not this table.detail string plus category and retryable. There is no structured details object with balances, currency lists, or route lists. Branch on code and retryable:
Validation Errors
Recipient field validation
Some recipient requirements are validated synchronously when you submit a payout (POST /v1/payouts) or create or update a beneficiary (POST /v1/beneficiaries / PUT /v1/beneficiaries/{id}). For example, an individual recipient paid to certain corridors must have both a first and last name, and account identifiers (IBAN, account number, routing number) must match the format the destination expects.
When a recipient fails one of these checks the API responds with 400 and a details array. Each entry names the failing field, a stable machine code, and a human-readable message:
code rather than the message text. Common codes:
These checks run before the payout is queued, so an invalid recipient is rejected immediately at submit time rather than failing later and being refunded. Fix the named field and resubmit.
Client-Side Validation
Validate data before API calls to catch problems without a round trip:clientRefis a duplicate guard, not a replay mechanism. Submitting a second payout with the sameclientRefreturns400with the messagePayout with this client reference already exists. It does not return the original payout. After an ambiguous failure (timeout, dropped connection), look the payout up withGET /v1/payouts?clientRef=<ref>before resubmitting, or use anIdempotency-Keyto get a true replay.- There is no global amount cap. The API constraint is format only: a positive decimal string with up to 8 decimal places. Real limits are per rail (for example mobile money: XOF 200–2,000,000, XAF 500–1,000,000, GHS 5–25,000) and per client, surfaced as
LIMIT_EXCEEDED. See Payouts for corridor-specific caps. - Currencies. EUR, USD, GBP, USDC, EURC, and NGN are enabled by default; USDT, XOF, XAF, and GHS are enabled via your account manager. ZMW, MZN, MWK, and EGP are sandbox-only. See the currency-to-route matrix in Payouts.
- EUR and GBP payouts need no route. They deliver to
ibanaccounts over Zuba’s international transfer network; the rail is selected automatically and surfaces asinternationalon payout reads. See Payment Routes.
Retry Strategies
Idempotent retries with Idempotency-Key
POST /v1/payouts accepts an optional Idempotency-Key header: a client-generated key, scoped to your workspace. This is the replay-safe retry mechanism:
- Same key + same body: replays the original response without creating a second payout.
- Same key + different body: rejected with
422 IDEMPOTENCY_KEY_REUSED. This indicates a bug in your client; never retry it with the same key. - Same key while the first request is still in flight: returns a retryable
409 IDEMPOTENCY_KEY_PROCESSING. Retry the identical request to receive the stored response. - Keys expire 24 hours after the original request completes; a repeat after expiry is treated as a new request.
Idempotency-Key when creating payouts. Without one, the envelope’s retryable flag is forced to false on the response, and a blind retry of a create that actually committed hits the clientRef duplicate guard (400) instead of getting the payout back.
Exponential Backoff
Retry on network errors and on responses whose envelope saysretryable: true, reusing the same key on every attempt:
retryable flag over hand-rolled status heuristics: it already encodes the safety rules, including the retryable 409 IDEMPOTENCY_KEY_PROCESSING (a 4xx a naive “never retry 4xx” rule would wrongly treat as permanent). On legacy endpoints without the envelope, retry only 429, 502, 503, 504, and network errors. Never blind-retry a POST that lacks idempotency protection.
Status Monitoring
Webhooks first
Webhooks are the primary status mechanism: Zuba pushes payout, deposit, and order status changes to your endpoint as they happen. Use polling only as a fallback (for example, to reconcile after webhook downtime).Polling as a fallback
Each resource has its own terminal statuses:
Note that
completed is not a payout status: the success state for a payout is paid.
404 as permanent (the payout does not exist); transient errors while polling can simply wait for the next interval.
Error Recovery
There is no automatic route-fallback to build client-side. Recover per scenario:PAYOUT_PROVIDER_UNAVAILABLE(503, retryable): retry the same request later with the sameIdempotency-Key, or callGET /v1/payouts/available-rails?accountId=<uuid>to check which rails currently serve the beneficiary’s account and submit a new payout (with a newclientRef) on a served rail.- Ambiguous outcome (timeout or dropped connection on create): if you sent an
Idempotency-Key, resend the identical request; the response is replayed. Otherwise, checkGET /v1/payouts?clientRef=<ref>before resubmitting. failedpayout: held funds are returned to your balance automatically. Fix the underlying cause (usually recipient details), then submit a new payout with a newclientRef.LIMIT_EXCEEDED: contact your account manager about your limits. Amounts above corridor caps are served by the orders channel.
Best Practices
- Send an
Idempotency-Keyon everyPOST /v1/payouts: it is the only replay-safe retry mechanism. - Branch on
code, never on message text:detailandmessagestrings can change; codes are stable. - Respect the
retryableflag: it encodes the retry-safety rules, including POST suppression without a key. - Subscribe to webhooks for status changes; poll only as a fallback.
- Log
request_idfrom every error response: it is the correlation id support uses to trace your request. - Rehearse failure paths in the sandbox with the documented deterministic failure values before going live.
Getting Help
When contacting support about a failed request, quote therequest_id from the problem envelope (also returned in the x-request-id response header). It lets support trace the exact request through Zuba’s systems.
Next Steps
- Implement webhook notifications for real-time status updates
- Test error scenarios end-to-end in the sandbox
- Review the Payouts concepts page for routes, currencies, and corridor limits