Skip to main content

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.
Zuba records each accepted deposit in the ledger and credits your balance once funds are received.

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
The call is idempotent per currency: if you already hold a live NGN account it is returned with a 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
The account details are also shown in the dashboard. 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 with POST /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:
The response contains the virtual account details to display to the sender:
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 in GET /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 method bank_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 with POST /v1/virtual-accounts:
Create Virtual Account Request
Issuance is asynchronous and gated on your Kenya business verification. 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/BIC CHFIKENX). 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 with POST /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.
The request returns a 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:
  • method must be mobile_money — KES does not accept the default manual_deposit.
  • amount must be a whole KES number greater than zero; M-Pesa does not settle fractional shillings.
  • phoneNumber is the payer’s MSISDN the prompt is sent to.
Poll 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 method virtual_account.

Requesting an account

Request an account over the API with POST /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
Branch on 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 with POST /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.
Live accounts enforce a minimum of 1 USDC or USDT per transfer. A smaller transfer is not credited, does not appear in GET /v1/deposits, and cannot be recovered. Contact your account manager if you have sent one. Amounts do not accumulate: two 0.60 transfers stay uncredited rather than combining to clear the minimum.Transfers funding a trade-desk order are exempt, so a shortfall top-up of any size still reaches your balance.The sandbox enforces no minimum, so sub-unit test transfers are credited there.
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 201 and opens the wallet; every later request for the same network and clientRef answers 200 with the same wallet, as long as at least one stablecoin on that network is still enabled for you. If none is, opening is refused with 403 METHOD_NOT_ENTITLED; read the wallet you already hold with GET /v1/wallets instead. Omit clientRef for your single default wallet on the network; pass distinct values (for example, your end-customer IDs) to open distinct wallets. A clientRef is 1 to 100 single-byte characters with no line break, must not begin or end with whitespace, and must not start with pool-.
  • A wallet is asset-agnostic: its address receives every stablecoin in assets. A wallet whose status is not active credits nothing, so its assets is 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-Id to 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 Conflict means the wallet for that owner, network and reference is frozen or archived and cannot be re-opened. Contact support.
Crypto deposits are availability-gated per environment. Contact your account manager to enable them. For the crypto payout side, see Payouts.

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 creditedAmountamount 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 with status, and page with limit (1–100, default 20) plus cursor:
Treat 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
Common cases: See the Error Handling guide for retry and recovery strategies.

Best Practices

  • Always include a unique clientRef for idempotency and reconciliation (duplicates are rejected with a 400)
  • Display the full metadata block 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
  • Poll GET /v1/deposits/{id} with exponential backoff
  • Handle all eight statuses, including in_review, refunding, and refunded
  • Treat in_review as not-yet-credited: funds are held for compliance review
  • Log deposit events for audit and debugging
  • 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