Overview
Payins fund your Zuba balance. These deposit methods are live today:- NGN bank transfers: transfers into your standing virtual account are credited automatically; dynamic per-deposit virtual accounts are also available via the API.
- EUR & GBP bank transfers: transfers into your dedicated named accounts (EUR IBAN via SEPA, GBP sort code + account number via Faster Payments) are credited automatically.
- USD bank deposits: transfers into your USD receiving account, over ACH, Fedwire or SWIFT, are credited automatically.
- KES deposits: fund your KES virtual account by bank transfer (PesaLink, RTGS, or SWIFT) or over M-Pesa — either an STK-push pull you initiate with
POST /v1/deposits, or a customer paybill deposit. Every rail credits the same account. - XOF & XAF bank transfers: receiving accounts fund your trade-desk orders. Upload evidence only when the returned settlement has
requirements.proofOfPayment: true; other orders reconcile without a proof upload. - Crypto deposits: provision an on-chain address and receive USDC or USDT, credited on confirmation. Live accounts apply a per-transfer minimum, below which a transfer is not credited.
NGN Deposits
Standing virtual account
Your workspace can hold a dedicated NGN virtual account in your business name (Zuba {Merchant Name}). Transfers sent to it are credited to your NGN balance automatically, and the same account number stays payable indefinitely.
Request one with POST /v1/virtual-accounts once your business verification is approved:
Create Virtual Account Request
200 instead of a 201. Currencies you name during onboarding are requested for you when your verification is approved, so you may already have one — fetch the details with GET /v1/virtual-accounts/me:
Get My Virtual Account Response
GET /v1/virtual-accounts/requirements?currency=NGN reports anything still outstanding before one can be issued.
Dynamic virtual accounts (per deposit)
Alternatively, create a deposit withPOST /v1/deposits to receive one-off virtual account details for a specific sender. NGN deposits created this way require the sender’s firstName, lastName, email, and phoneNumber to generate the virtual account:
Create Deposit Response (NGN)
The
iban and bic field names are shared across deposit methods. For NGN
deposits, iban carries the virtual account number and bic carries the
bank name. Display the metadata block to the sender: accountNumber,
accountName, bankName, and expiresAt are the details they need to
complete the transfer before the virtual account expires.USD Bank Deposits
A USD receiving account is held in your business name and reachable over three rails: ACH, Fedwire, and SWIFT. Transfers sent to its account and routing numbers are credited to your USD balance automatically, with no create call per transfer, and appear as deposits inGET /v1/deposits.
The account details are shown in the dashboard and returned by the API once the account is active.
EUR & GBP Bank Deposits
Your workspace can be provisioned with dedicated named accounts in your business name: an EUR IBAN reachable over SEPA and a GBP account number + sort code reachable over Faster Payments. Transfers sent to them are credited to the matching balance automatically (no create call is required) and appear as deposits with methodbank_deposit in GET /v1/deposits and on the deposit.completed webhook. Deposits created before this field settled carry virtual_account instead, so match on both values when reading a full deposit history.
The account details are shown in the dashboard. Contact your account manager to provision named accounts.
KES Deposits
Every KES collection is received into a KES virtual account — it is the prerequisite for all the methods below, and each of them settles funds into it. Request one withPOST /v1/virtual-accounts:
Create Virtual Account Request
GET /v1/virtual-accounts/requirements?currency=KES reports anything still outstanding before one can be issued — see Requesting an account for the full flow. Every deposit below appears in GET /v1/deposits.
Bank transfer
Send funds to your KES virtual account number over PesaLink, RTGS, or SWIFT (SWIFT/BICCHFIKENX). Transfers are credited to your KES balance automatically — no create call per transfer — and appear with method virtual_account.
Mobile money (M-Pesa)
Two ways to collect over M-Pesa, both crediting the same KES virtual account.STK push
Pull a specific amount by prompting the payer’s phone: create a deposit withPOST /v1/deposits, method: "mobile_money", and the payer’s phoneNumber. The payer keys in their M-Pesa PIN on the prompt to approve the transfer.
processing deposit once the prompt is sent; the credit is confirmed asynchronously after the payer approves it:
Create Deposit Response (KES STK push)
KES STK-push deposits have three input rules:
methodmust bemobile_money— KES does not accept the defaultmanual_deposit.amountmust be a whole KES number greater than zero; M-Pesa does not settle fractional shillings.phoneNumberis the payer’s MSISDN the prompt is sent to.
GET /v1/deposits/{id} for the outcome — the deposit moves to completed once the payer approves the prompt and M-Pesa settles, or failed if the prompt is declined or times out.
Paybill deposit
The payer pushes funds themselves: they pay the Zuba paybill, entering your KES virtual account number as the account number. No create call is needed — the credit is matched to your account and appears with methodvirtual_account.
Requesting an account
Request an account over the API withPOST /v1/virtual-accounts, for your own workspace or for a customer account you manage (pass its id in Zuba-Account-Id).
Issuance is asynchronous: the request answers immediately with a pending account, and the account becomes usable once the receiving bank opens it. Subscribe to the virtual_account.active webhook to learn when its details are available, and virtual_account.failed to learn that it will not be opened. Requesting again while one is pending returns the same account rather than opening a second.
An account can only be opened once the owner has an approved application carrying everything the receiving bank asks for. Those attributes vary by currency and are not all collected during onboarding, so rather than discovering a gap from a failed request, ask first:
Check what is outstanding
Requirements Response
ready. When it is false, each entry names what is outstanding and, in resolution, the exact call that supplies it — already resolved down to the specific person where the value belongs to one. Send those, then request the account.
Read code rather than description: the codes are stable, the prose is not. A requirement whose resolution is null is one no API call sets — a detail recorded when the application was reviewed, acceptance the account owner owes for the terms the account would be opened under, or a state of the account or workspace rather than a value. Its description says what clears it.
The disclosure.* codes are that acceptance: the partner bank’s own documents, which the account owner agrees to before the account can be opened. One code covers the whole outstanding package, and the documents travel with it in disclosures — slug, title, version, a link to the PDF, the order to show them in, and the exact wording each acceptance stands for — so you render them without a second call. disclosures is null on every other code. Acceptance is collected while the owner’s application is still open, which is where the onboarding flow already asks for it; once an application is approved its acceptances are frozen, so an owner still owing one after approval needs support. That is why the resolution is null rather than a call you could make.
status distinguishes the cases where nothing is outstanding: ready (the request would succeed), already_exists (the owner already holds an account in this currency, and requesting one returns it), and unavailable (the currency is not offered to this owner, and no value they supply changes that).
The check is read-only and safe to call as often as you like — collecting these from your customer at signup avoids a failed request later.
Crypto Deposits
Open a wallet withPOST /v1/wallets, then send USDC or USDT to its address. The deposit is recorded and credited once the transfer confirms on-chain, and appears with method crypto_deposit. GET /v1/wallets lists the wallets you hold, newest first with cursor pagination; GET /v1/wallets/{id} reads one.
network names the chain. Pass the CAIP-2 chain identifier or any of the friendly spellings beside it, in any casing; both POST /v1/wallets and the ?network= filter on GET /v1/wallets accept either.
Every wallet comes back with the CAIP-2 identifier in
network whichever spelling you sent, and the chain’s readable name in networkName for display. Key your own logic on network.
The network values listed in the API reference but absent from this table are test networks or networks enabled for individual accounts; a request for one you are not enabled on is refused with METHOD_NOT_ENTITLED. Each wallet’s assets lists the stablecoins your account is credited for on that network.
Wallet behaviour:
- One wallet per owner, network and reference. The first request answers
201and opens the wallet; every later request for the same network andclientRefanswers200with the same wallet, as long as at least one stablecoin on that network is still enabled for you. If none is, opening is refused with403 METHOD_NOT_ENTITLED; read the wallet you already hold withGET /v1/walletsinstead. OmitclientReffor your single default wallet on the network; pass distinct values (for example, your end-customer IDs) to open distinct wallets. AclientRefis 1 to 100 single-byte characters with no line break, must not begin or end with whitespace, and must not start withpool-. - A wallet is asset-agnostic: its address receives every stablecoin in
assets. A wallet whosestatusis notactivecredits nothing, so itsassetsis empty. - A wallet credits deposits only on the network it was opened on. An EVM address is syntactically valid on every EVM chain, but a transfer sent to it on another chain is not credited and cannot be recovered. Open a separate wallet per network.
- Sub-accounts hold their own wallets. Pass
Zuba-Account-Idto open, list, or read a sub-account’s wallets; deposits to them settle into that sub-account’s balance. Without the header you act on, and see, your master account’s wallets only. - A
409 Conflictmeans the wallet for that owner, network and reference is frozen or archived and cannot be re-opened. Contact support.
Deposit Lifecycle
What the Deposit Credits
amount is what arrived. Where your plan prices deposits, Zuba’s charge is
serviceFee and your balance moves by creditedAmount — amount less that
fee, in the deposit’s own currency. Both fields are on every deposit from
GET /v1/deposits/{id} and on the deposit.completed webhook, and
serviceFee is "0.00000000" where your plan prices no deposits.
Credit your own customer from creditedAmount. Reconciling against amount
over-credits by the fee on every priced deposit.
Deposit Statuses
Checking Deposit Status
Deposit IDs are UUIDs. Fetch a deposit by ID to get its current status:A deposit’s terminal outcome is delivered as a webhook: subscribe an endpoint
to
deposit.completed (funds credited) and deposit.failed (the collection
terminally failed, e.g. a declined M-Pesa prompt) — see the
Webhooks guide. No webhook fires for the
pending/processing stages; to follow those, poll GET /v1/deposits/{id},
which also triggers an asynchronous status refresh on a pending or
processing deposit.Listing Deposits
Retrieve deposits for your account as a paginated list, newest first. Filter withstatus, and page with limit (1–100, default 20) plus cursor:
nextCursor as an opaque token: pass it back verbatim in the cursor query parameter to fetch the next page, and stop when hasMore is false. An invalid cursor, limit, or status value returns a 400 with field-level details.
If you manage customer accounts, pass Zuba-Account-Id: <accountId> to list only that account’s deposits; without the header the list covers the whole workspace. Each deposit carries an accountId naming the account it was credited to (null for your own workspace-level deposits).
Error Handling
Payin endpoints return standard error bodies:400 Bad Request
See the Error Handling guide for retry and recovery strategies.
Best Practices
Deposit Creation
Deposit Creation
- Always include a unique
clientReffor idempotency and reconciliation (duplicates are rejected with a400) - Display the full
metadatablock to the sender: account number, account name, bank name, and expiry - Communicate the virtual account expiry (
metadata.expiresAt) so the sender transfers in time - Store deposit IDs (UUIDs) for status tracking and customer support
Status Monitoring
Status Monitoring
- Poll
GET /v1/deposits/{id}with exponential backoff - Handle all eight statuses, including
in_review,refunding, andrefunded - Treat
in_reviewas not-yet-credited: funds are held for compliance review - Log deposit events for audit and debugging
Security
Security
- Use HTTPS for all API communications
- Never log sensitive customer payment data
- Implement proper access controls for deposit endpoints
- If you consume webhooks for other events, verify signatures (see Verifying Signatures)
Next Steps
Payouts
Send money globally to beneficiaries
Webhook Setup
Real-time notifications for payout, order, and account events
Ledger System
Track transactions with double-entry accounting
API Reference
Complete API documentation