Skip to main content
Robust error handling is essential in a payment integration. This guide documents Zuba’s error response formats, the stable error-code registry, safe retry semantics with idempotency keys, and recovery patterns for payouts, deposits, and orders.

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 a 400 with a details array 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 with Content-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 are 400 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:
Legacy bodies carry no 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.
The envelope carries a human-readable 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:
Branch on 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:
A few semantics worth encoding in your client:
  • clientRef is a duplicate guard, not a replay mechanism. Submitting a second payout with the same clientRef returns 400 with the message Payout with this client reference already exists. It does not return the original payout. After an ambiguous failure (timeout, dropped connection), look the payout up with GET /v1/payouts?clientRef=<ref> before resubmitting, or use an Idempotency-Key to 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 iban accounts over Zuba’s international transfer network; the rail is selected automatically and surfaces as international on 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.
Always send an 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 says retryable: true, reusing the same key on every attempt:
Prefer the envelope’s 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.
Treat a 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 same Idempotency-Key, or call GET /v1/payouts/available-rails?accountId=<uuid> to check which rails currently serve the beneficiary’s account and submit a new payout (with a new clientRef) 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, check GET /v1/payouts?clientRef=<ref> before resubmitting.
  • failed payout: held funds are returned to your balance automatically. Fix the underlying cause (usually recipient details), then submit a new payout with a new clientRef.
  • LIMIT_EXCEEDED: contact your account manager about your limits. Amounts above corridor caps are served by the orders channel.
Never split a payout into smaller chunks to get under a limit.

Best Practices

  1. Send an Idempotency-Key on every POST /v1/payouts: it is the only replay-safe retry mechanism.
  2. Branch on code, never on message text: detail and message strings can change; codes are stable.
  3. Respect the retryable flag: it encodes the retry-safety rules, including POST suppression without a key.
  4. Subscribe to webhooks for status changes; poll only as a fallback.
  5. Log request_id from every error response: it is the correlation id support uses to trace your request.
  6. 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 the request_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