Skip to main content

Overview

Payouts are the core functionality of the Zuba platform, enabling you to send money to beneficiaries worldwide through various payment rails including international transfers, SWIFT, US domestic rails (ACH and Fedwire), crypto networks, local bank transfers, and mobile money.

Payout Lifecycle

Beneficiaries & Accounts

Beneficiary Management

Before sending payouts, you must create beneficiaries with their payment details:

Account Types

Zuba supports multiple account types for maximum flexibility:
For Ghana, Kenya, Uganda, Cameroon, Côte d’Ivoire and South Africa, bankCode is Zuba’s internal bank identifier. Fetch the supported banks and their codes for a country from GET /v1/banks?country=<ISO-3166-1-alpha-2> (e.g. ?country=CM), which returns [{ "name", "bankCode" }] sorted by name. The list is scoped to banks Zuba can currently pay, so for Kenya and Uganda it is the payable subset rather than every registered bank. For South Africa it is every supported bank, and paying one requires ZAR payouts to be enabled for your account. For Nigeria, the default when country is omitted, bankCode is the CBN short code or the NIP long code, and a beneficiary account accepts either.
EUR (IBAN)bic is optional on iban accounts.
GBP (IBAN)GBP beneficiaries also use the iban account type, with a GB IBAN:
US Bank Account (USD: ACH / Domestic Wire)
Nigerian Bank AccountbankCode accepts the 3-digit CBN short code (e.g. 044) or the 6-digit NIP long code (e.g. 000014); crAccount is the 10-digit account number.
Ghanaian Bank AccountbankCode is Zuba’s internal Ghana bank identifier (e.g. gh_0001); crAccount is the 8–20 digit account number.
Kenyan Bank AccountbankCode is Zuba’s internal Kenya bank identifier (e.g. ke_0001 for KCB, ke_0003 for Absa); crAccount is the 5–20 digit account number. KES is dual-rail — the same currency also pays out to a mobile money wallet (see the Mobile Money tab).
South African Bank AccountbankCode is Zuba’s internal South Africa bank identifier (e.g. za_0001 for Standard Bank, za_0002 for FNB / RMB); crAccount is the 5–20 digit account number. accountType is optional: current, savings or transmission, defaulting to current when omitted. Send it when you know it, because the receiving bank validates the account number against its type. A business beneficiary paid to a South African bank account must also carry its company registrationNumber. A ZAR amount carries at most two decimal places; a finer amount is rejected at creation. ZAR payouts are enabled per account: until a ZAR rate is set up for yours, a ZAR payout is refused with CORRIDOR_NOT_ENABLED. Contact your account manager to enable them.
Cameroonian Bank AccountbankCode is Zuba’s internal Cameroon bank identifier (e.g. cm_0001 for Afriland First Bank, cm_0006 for BGFIBANK); crAccount is the 8–24 digit account number. XAF is dual-rail — the same currency also pays out to a mobile money wallet (see the Mobile Money tab).
XAF payouts require the sender’s KYC on both rails. An individual sender must supply, in senderInfo: firstName and lastName, phoneNumber in international format, fundOrigin (SALARY, BUSINESS, or INVESTMENT), gender (M or F), and country (the sender’s own country as an ISO 3166-1 alpha-2 code), or the payout is rejected at creation. The same set applies to XAF mobile money — see the Mobile Money tab. To send as your own account, omit senderInfo entirely — the originator is then the paying account as its KYB application was approved (your workspace, or the sub-account named in Zuba-Account-Id). An account approved as an individual pays as that person, so on XAF it cannot omit the block: it supplies the individual set above. Supply senderInfo otherwise only to pay out on behalf of a distinct third party; a business third-party sender must include companyName, registrationNumber, and country (its jurisdiction of incorporation).
XAF Bank Payout (Cameroon)
Ivorian Bank AccountbankCode is Zuba’s internal Côte d’Ivoire bank identifier (e.g. ci_0009 for Ecobank, ci_0013 for Société Générale); crAccount is the 8–24 digit account number. XOF is dual-rail — the same currency also pays out to a mobile money wallet (see the Mobile Money tab), and the bank rail serves Côte d’Ivoire only.
The per-transaction amount is XOF 200–2,000,000.Côte d’Ivoire bank payouts use the sender’s name. Supply firstName and lastName in senderInfo for an individual sender (or companyName for a business sender). If senderInfo is omitted, the paying account’s approved name is used as the originator instead. Unlike Cameroon, Côte d’Ivoire does not need phoneNumber, fundOrigin, or gender.Ugandan Bank AccountbankCode is Zuba’s internal Uganda bank identifier (e.g. ug_0020 for Stanbic, ug_0002 for Absa); crAccount is the 5–20 digit account number. UGX is dual-rail — the same currency also pays out to a mobile money wallet (see the Mobile Money tab). The Ugandan shilling has no minor unit, so amount must be a whole number on both rails.
The per-transaction minimum is UGX 1, with no published ceiling.
UGX bank payouts are in early access — a gated corridor. An unpinned workspace’s UGX bank payout is rejected at creation with RAIL_UNAVAILABLE. UGX mobile money is gated on the same corridor. Enablement can be granted per rail, so ask your account manager for the rails you need.
Argentine Bank Account (ARS)
ARS payouts are in beta — a gated corridor. Contact your account manager to have it enabled for your workspace; until it is, a payout to ARS is rejected at creation.
An Argentine beneficiary is identified by a single account string plus the kind of identifier it is. accountNumber, accountType and accountHolderName are all required. accountType is not inferred: a CBU and a CVU are both 22 digits and cannot be told apart by shape.
ARS payouts additionally require the sender’s identity. Supply, in senderInfo: firstName, lastName, and dateOfBirth (YYYY-MM-DD). This is required whether you are paying as your own workspace or on behalf of a third party.
ARS Bank Payout (Argentina)
Sandbox-only currencies
ZMW (Zambian kwacha), MWK (Malawian kwacha) and EGP (Egyptian pound) are available in the sandbox only. Payouts to them settle with simulated outcomes. See Sandbox testing. In production these currencies are not yet available and a payout to one is rejected at creation.MZN (Mozambican metical) is simulated in the sandbox too, over mobile money rather than a bank account. See Mozambique (MZN) mobile money. In production an MZN payout is rejected at creation unless your workspace has been enabled for it.
Zambian Bank Account (sandbox only)
The same shape applies for MWK and EGP: any bankCode is accepted in the sandbox.

Creating Payouts

Single Payout

After creating a beneficiary, use their ID to create payouts (recommended approach). Currency Fields: inputCurrency is the currency from your account you’ll be paying from, while currency is what the beneficiary will receive (automatic conversion if different):
senderInfo is optional. When omitted, the originator is the paying account as its KYB application was approved: your workspace, or the sub-account named in Zuba-Account-Id (see Sending from a sub-account). An account approved as a business is named by its legal name, and one approved as an individual by that person. Some corridors require senderInfo: GHS mobile money needs the sender’s first and last name, and XAF mobile money needs the sender’s name and phone number. On a crypto payout an individual sender must also carry dateOfBirth; see Travel Rule.

When a quote is required

A quote (POST /v1/quotes, passed as quoteId) is required only for payouts funded from an NGN balance into USD, EUR or GBP. On every other pair a quoteId is optional: pass one to lock the rate for the payout, or omit it and the payout is priced at the current rate. A payout quote fixes one side of the pair. Send toAmount to fix what the beneficiary receives, or fromAmount to fix what you are debited before the payout fee; the quote returns the other side. Quotes from an NGN balance into USD, EUR or GBP take toAmount only. Create the payout with amount equal to the quote’s targetAmount, which a fromAmount quote on a cross-currency pair rounds down to the smallest unit every payout method in that currency accepts: whole units for currencies with no minor unit, such as XOF, XAF and UGX, and whole shillings for KES. On a same-currency pair targetAmount equals fromAmount. The payout fee is not part of the quote: it is added to the debit when the payout is created, whichever side you fixed. A cross-currency payout on a pair listed by GET /v1/fx/cross-rate-quotes/corridors goes out only where your pricing policy covers it; a listed pair nothing prices is rejected with CORRIDOR_NOT_ENABLED, with or without a quote. On such a pair the amount must also sit inside that corridor’s per-transaction band, or the payout is rejected with AMOUNT_OUT_OF_BOUNDS. Same-currency payouts carry no band on the payout path. Sends into NGN require your policy to price the pair, with or without a quote, whichever currency funds them. A cross-currency pair the listing has never carried is priced at the current rate whether or not your policy prices it, except a CHF payout. A CHF payout is paid only from an XOF balance to a Swiss IBAN: from any other balance it is rejected with CORRIDOR_NOT_ENABLED, to an IBAN from another country with IBAN_COUNTRY_NOT_SUPPORTED, and on the swift or crypto route with RAIL_UNAVAILABLE. No published route value sends a CHF payout yet; CHF is enabled per client on request. A pair the listing carries for other clients but not for you is one your policy does not cover, and is refused. A pair whose stored rate is zero or negative is rejected with INSUFFICIENT_LIQUIDITY; a pair with no stored rate at all, or whose rate has expired, is rejected with a 400 carrying the message FX rate not available for <source> to <destination> conversion and no error code. When your source balance is held in an account of your own that cannot fund the payout, the payout is refused before anything is debited. With a quoteId it is rejected with 422 and one of these codes: FUNDING_ACCOUNT_NOT_ACTIVE (the account is frozen), FUNDING_ACCOUNT_AMBIGUOUS (your balance in the source currency is held in more than one place; contact support to resolve it), or FUNDING_ACCOUNT_UNAVAILABLE (the account cannot be reached; try again later). The quote stays unused, so you can send the payout again with it while it is valid. Without a quoteId, the same causes return 201 with the payout failed and its failureReason naming the cause. A CAD payout is paid only from a EUR or USD balance, to a Canadian bank account: a bank_account in CAD whose data carries accountNumber (5–12 digits), institutionNumber (3 digits), transitNumber (5 digits) and accountHolderName, with an optional bankName, and the bank’s bic (8 or 11 characters, with CA as characters 5 and 6). The bic may be left out for Bank of Montreal (001), Scotiabank (002), Royal Bank of Canada (003), TD Canada Trust (004), National Bank of Canada (006), CIBC (010), Laurentian Bank (039), ATB Financial (219) and Desjardins (815), whose BIC is filled in. Spaces and dashes in the numbers are accepted and removed. The recipient needs a full address (address, city and postcode); a province in countrySubdivision is sent when given. From any other balance a CAD payout is rejected with CORRIDOR_NOT_ENABLED; a missing number with MISSING_BANK_ACCOUNT_DETAILS, and a malformed one with INVALID_ACCOUNT_NUMBER or INVALID_ROUTING_NUMBER on its own field; a missing bic with MISSING_BIC, and a malformed one with INVALID_BIC. Each payout is between 1 and 250,000 CAD. No published route value sends a CAD payout yet; CAD is enabled per client on request, with the route to send.
Sending without a quoteId removes the quote’s single-use protection: one quote can fund at most one payout, so a retried request carrying the same quoteId cannot pay twice. On a payout sent without one, that guard is the Idempotency-Key header. Send an Idempotency-Key on any request you might retry, including a retry after a timeout where you never saw the response.

Batch Payouts

Process multiple payouts in a single API call. Every payout in the batch is validated independently and requires its own route:

Sending from a sub-account

If your workspace has sub-accounts, pass the sub-account’s ID in the optional Zuba-Account-Id header on POST /v1/payouts to send the payout from that sub-account’s balance: the amount and fee are debited from the sub-account, and the payout records which sub-account it belongs to. Omit the header to send from your master (workspace) balance, exactly as the examples above. A sub-account payout that omits senderInfo names the sub-account as the originator, as its own KYB application was approved: a business by its legal name and registration number, an individual by their name. The name is what the payment provider receives; the registration number and date of birth go to screening and the Travel Rule. It is never your master workspace’s identity. A payout that omits senderInfo is rejected at creation with ORIGINATOR_NOT_ON_RECORD when the paying account, master or sub-account, has no originator of record:
  • it holds no approved KYB application;
  • an account approved as a business holds no legal name on its application;
  • a sub-account approved as a business holds no registration number on its application;
  • an account approved as an individual holds no first or last name on its application;
  • on a crypto payout, an account approved as an individual holds no date of birth on its application.
Supply senderInfo to name a third party, exactly as from the master. The named sub-account must be one you own and must be active. A header naming an account you do not own is rejected with 403 not_account_owner, and a sub-account that is not yet active is rejected with 422 ACCOUNT_NOT_ACTIVE. Sub-accounts are a gated feature — contact your account manager to enable them.

Payment Routes

Select the rail with the route field on each payout request:
EUR and GBP payouts are delivered to IBAN accounts over Zuba’s international transfer network. The rail is selected automatically from the beneficiary’s iban account and surfaces as international on payout reads and in the dashboard. The former sepa_credit / sepa_inst route values have been retired and are no longer routable.

Crypto rail

Set route: "crypto" to send a stablecoin (USDC or USDT) on-chain to a crypto wallet beneficiary. The currency is the stablecoin and is debited from your same-currency balance (no FX). The destination network and address come from the beneficiary’s wallet account (see Crypto Wallets above), so the request only references the beneficiary by id:
Crypto Payout
The funds are held the moment the payout is created, screened for compliance, then signed and broadcast on-chain. The payout reaches paid only once the transaction has broadcast; if it is declined or signing fails, the held funds are returned to your balance automatically. Crypto payouts are availability-gated per environment. Contact your account manager to enable them. Once the transfer is broadcast, GET /v1/payouts/{id} returns the on-chain transaction hash in the txHash field (null for non-crypto payouts or before broadcast). Use it to look the transfer up on a block explorer for the payout’s network.

Travel Rule

In production every crypto payout is screened under the Travel Rule, whatever the amount, so both parties must be identifiable. Omit senderInfo and the originator is the paying account as its KYB application was approved (your workspace, or the sub-account named in Zuba-Account-Id). An account approved as a business needs no date of birth. One approved as an individual must hold a date of birth on its application, or the payout is rejected at creation with ORIGINATOR_NOT_ON_RECORD, as is any account with no originator of record (see Sending from a sub-account). Otherwise: The beneficiary refusal is a 400 whose message names dateOfBirth and carries no code. Add the date of birth with PUT /v1/beneficiaries/{id} and resubmit. The sandbox does not run Travel Rule screening, so a crypto payout that succeeds there without a date of birth is refused in production.

Mobile money (XOF, XAF, GHS, KES, and UGX)

Set route: "mobile_money" to pay out XOF to a beneficiary’s mobile money wallet across West Africa, XAF to a wallet in Cameroon, GHS to a wallet in Ghana, KES to a wallet in Kenya, or UGX to a wallet in Uganda. The destination provider and phone number come from the beneficiary’s mobile account (see Mobile Money under Account Types above):
Mobile Money Payout
Supported in Côte d’Ivoire, Senegal, Mali, Burkina Faso, Benin, and Togo (XOF), Cameroon (XAF), Ghana (GHS), Kenya (KES), and Uganda (UGX). The per-transaction amount depends on the destination currency: XOF 200–2,000,000, XAF 500–1,000,000, GHS 5–50,000, KES 1–250,000, UGX 1–5,000,000. A mismatched provider/country, an invalid phone number, an unsupported country, a currency that isn’t XOF/XAF/GHS/KES/UGX, or an out-of-range amount is rejected at creation. UGX amounts must be whole numbers. The Ugandan shilling has no minor unit, so a fractional amount is rejected at creation on both the mobile and bank rails. Ghana (GHS), Kenya (KES), Uganda (UGX), Cameroon (XAF), and Côte d’Ivoire (XOF) are dual-rail. The same currency also pays out over a bank account (route: "bank_transfer"); the rail is chosen by the beneficiary’s account type (mobile vs bank_account). XOF mobile money covers six West African countries but the XOF bank rail serves Côte d’Ivoire only. XAF payouts carry extra sender KYC on both rails (fundOrigin, gender, and country) — see the Bank Account tab. GHS mobile requires the sender’s first and last name via senderInfo (no phone, unlike Cameroon), or the payout is rejected:
GHS Mobile Money Payout (Ghana)
Kenya (KES) and Uganda (UGX) need no senderInfo. Uganda is in early access on either rail — see the gate note under Mobile Money in Beneficiary account types. Supply the beneficiary and amount only:
KES Mobile Money Payout (Kenya)
The same shape applies to Uganda, with a whole-number amount:
UGX Mobile Money Payout (Uganda)
Cameroon (XAF) additionally requires the sender’s KYC. An individual sender must supply firstName, lastName, phoneNumber, fundOrigin, gender, and country in senderInfo — the same set the bank rail requires — or the payout is rejected at creation:
XAF Mobile Money Payout (Cameroon)
XOF, XAF, GHS and KES mobile money are open. UGX is in early access on both rails, and an unpinned workspace is rejected at creation with RAIL_UNAVAILABLE. Enablement can be granted per rail, so ask your account manager for the rails you need.

USD rails

For USD payouts, three rails are surfaced by GET /v1/payouts/available-rails?accountId=<uuid>:
  • fedwire: domestic wire to a US bank. Requires routing + account number on the beneficiary.
  • ach: batched same-day-to-3-day transfer to a US bank. Same required fields as fedwire.
  • swift: international wire via SWIFT. Requires BIC + IBAN/account number + the beneficiary’s bank country. Destination coverage spans most major markets and expands as corridors are enabled, so it is not a fixed list: GET /v1/payouts/available-rails?accountId=<uuid> reports, per saved account, whether SWIFT currently serves that account’s destination country (available plus an unavailableReason when it does not).
Over the API you pass route on every payout request; the beneficiary account just has to match it: a swift account for route "swift", a US bank_account for "ach" / "fedwire". In the dashboard, the rail chosen when the account was saved is remembered so senders aren’t re-prompted. Beneficiary accounts intended for SWIFT must be created with type: "swift" and supply swiftCode, accountNumber, beneficiaryCountry, and (optionally) bankName / beneficiaryAddress in data. SWIFT payouts also require a purpose-of-payment document: request an upload URL from POST /v1/payouts/pop-documents/upload-url, upload the document, and pass the returned key as purposeOfPaymentDocumentKey on the payout. A SWIFT payout without it is rejected at creation. A SWIFT payout funded from an NGN balance is a cross-rate send: it passes through a quote (POST /v1/quotes), is debited in the source currency, and Zuba funds the USD outflow to the destination bank. A same-currency USD→USD SWIFT payout needs no quote and is sent 1:1 from your USD balance.

Route Selection Logic

The rail is determined by the beneficiary’s account type and currency:

Tracking & Status

Payout Statuses

Cancelling a payout

POST /v1/payouts/{id}/cancel cancels a payout that is still created or queued. The balance debited at creation (principal, fee and any conversion legs) is returned to the account it was taken from, and a payout.cancelled webhook is delivered. Retrying a cancel is always safe: an already-cancelled payout is returned again, and a balance return that had not completed is driven to completion. If the balance return does not complete, the call responds 500 INTERNAL_ERROR while the payout is already cancelled; the payout is flagged for our operators and a later cancel call completes the return. A payout in any status other than created, queued or cancelled is refused with 409 PAYOUT_NOT_CANCELLABLE; check its status with GET /v1/payouts/{id} instead. From the dashboard, a receipt can be downloaded for a payout in any status except cancelled. For a payout that is still in progress, the receipt reflects its current status.

Amounts above corridor limits

Every corridor has a per-transaction band, and a payout or payout quote outside it is rejected by the standard bounds checks. Amounts above corridor limits are served by the orders channel (POST /v1/orders) for order-enabled accounts: a quote-first flow where funds are held from your balance and the trade is executed by Zuba’s desk within a stated execution window. See the Trade Desk Orders guide or contact us to enable orders on your account.

Real-time Tracking

Best Practices

  • Always validate beneficiary details before creating
  • Use GET /v1/payouts/requirements?currency=USD&route=swift to check what the corridor requires before submitting. It reports the beneficiary fields and the senderInfo keys, each for an individual and for a business, and whether senderInfo can be omitted at all. Pass country with route=mobile_money to narrow the sender requirements to one destination country of a currency, such as the sender phone a mobile-money payout into Cameroon carries, or with route=swift for the recipient rules of the provider that pays out to the recipient’s country
  • Validate each field against the rules the requirements response reports for the recipient type. A corridor that addresses an individual recipient by first and last name separately reports a name rule and refuses a single-token name at submit, with a 400 naming name. A business recipient is its legal name and needs no split
  • Keep description within the payout.descriptionMaxLength the same response reports: the strictest narration limit among the rails serving the currency (18 characters for GBP and GHS), counted after accented and special characters are converted for the payment rail (a crypto payout counts the text as entered). A longer value is refused at submit with a 400 whose detail carries the code NARRATION_TOO_LONG
  • Store beneficiary IDs for repeat payments
  • Keep beneficiary information up to date
  • Omit senderInfo to attribute the paying account as its KYB application was approved (your workspace, or the sub-account named in Zuba-Account-Id) where sender.senderInfoRequired is false for the corridor; supply it to pay out on behalf of a third party, and on a corridor that reports true, such as mobile money into Cameroon, which needs a sender phone no account record carries
  • An account approved as an individual pays as that person, so on a corridor with rules for individual senders (XAF) it supplies the sender.individual fields even where senderInfoRequired is false
  • Implement proper error handling for all API calls
  • Use exponential backoff for retries
  • Monitor webhook notifications for status updates
  • Log all transactions for audit purposes
  • Never expose API keys in client-side code
  • Validate webhook signatures
  • Use HTTPS for all API communications
  • Implement proper access controls

Next Steps

First Payout Guide

Step-by-step guide to send your first payout

Batch Processing

Learn how to process multiple payouts efficiently

Webhooks

Set up real-time notifications for payout status

API Reference

Complete API documentation for payouts