- Quote-first. Every order starts from a firm quote (
POST /v1/quoteswithintent: "order"). There is no auto-priced path. - Fund-first. The desk executes only against funds that are already secured. If your Zuba balance covers the order it is funded at creation; otherwise the order returns funding instructions and waits for your transfer. Zuba never extends credit, and nothing moves outbound until the order is funded.
- Manually executed. A human trading team fills the order and a second operator approves the execution before any funds move. Orders complete within the execution window returned on the order, not in seconds.
Orders vs payouts
Prerequisites
- Orders enabled on your account. Orders are enabled per account, configured with the currency pairs (corridors) and transaction limits agreed during onboarding. Contact your account manager to get set up.
- A beneficiary holding an active account in the buy currency (see Beneficiaries).
- Valid API credentials (see Authentication).
https://api.sandbox.zuba.com. In production, swap it for https://api.zuba.com; everything else is identical.
Step 1: Get a firm quote
Request a quote withintent: "order". Fix exactly one side of the trade:
fromAmountfixes what you sell (the total debit), ortoAmountfixes what your beneficiary receives.
- The rate is all-in.
totalDebitAmountis exactly what the order costs you; there is no separate fee added at order creation.rateis expressed asfromCurrencyper 1toCurrency. - The quote is firm. The rate you see is the rate your order executes at, regardless of market movement while the desk works the order.
- The quote is single-use and short-lived. It typically expires within about 30 seconds; always read
expiresAtrather than assuming a TTL. Create the order before it expires, or request a fresh quote.
UNSUPPORTED_CORRIDOR if the currency pair is not available on the orders channel, or with AMOUNT_OUT_OF_BOUNDS if the amount is below the corridor minimum.
Step 2: Create the order
Create the order with the quote id and a beneficiary. TheIdempotency-Key header is required.
fromAmount, it is debited into a hold reserved for this order in the same step, and the order is created directly in funds_received, queued for the desk:
awaiting_funds and the response carries a settlement block with the funding instructions, the exact amount to transfer, and a unique reference:
quotationId returns the order it already created), and clientRef is unique per account (reusing one on a different order returns 409 CLIENT_REF_ALREADY_USED).
Step 3: Fund the order
Balance-funded orders skip this step; they are alreadyfunds_received.
For a settlement-funded order, transfer requiredAmount to the instructions before fundingDeadlineAt (mirrored on settlement.expiresAt):
- Bank transfer (
method: "virtual_account"): send to the account ininstructionsand include thereferencein the transfer narration wherever your bank supports one. How funding is confirmed depends on the account, stated ininstructions.note:- Narration-matched accounts: a referenced transfer attributes to the order at any amount — a short payment sits on your balance until you top it up under the same reference, and an overpayment attributes with the excess left on your balance. Without the reference, only a transfer of exactly
requiredAmountauto-matches; anything else is credited to your balance and attributed manually. - Confirmation-upload accounts (the note asks you to upload your payment confirmation): after sending the transfer, request an upload URL with
POST /v1/orders/{id}/settlement/proof-upload-url(body: the document’scontentTypeandcontentLength; PDF, JPG or PNG, up to 10 MB),PUTthe confirmation document to it, then submit the returnedproofKeywithPOST /v1/orders/{id}/settlement/proof. Funding is confirmed from the uploaded document, and the order moves on automatically once it is verified. Still include thereferenceon the wire: it speeds up verification.
- Narration-matched accounts: a referenced transfer attributes to the order at any amount — a short payment sits on your balance until you top it up under the same reference, and an overpayment attributes with the excess left on your balance. Without the reference, only a transfer of exactly
- Crypto transfer (
method: "wallet_address"): send exactlyrequiredAmountto your own deposit address frominstructions.addresses(any listed network). The receiving address identifies you, so no reference is needed.
funds_received, executionDeadlineAt is set, and the order.funds_received webhook fires.
In the sandbox, no real bank transfer exists to verify, so a confirmation-upload settlement is confirmed immediately when you submit the proof document — upload any PDF/JPG/PNG and the order moves to funds_received on submission, letting you integration-test the full awaiting_funds lifecycle. Narration-matched funding is simulated with POST /v1/sandbox/deposits carrying the settlement reference in narration.
If the funding window closes first, the order lands expired (webhook order.expired) and the settlement is closed. Nothing was held, so there is nothing to refund; a transfer that arrives after expiry is simply credited to your balance. On confirmation-upload accounts this includes a confirmation you submitted before expiry: it is still verified and the funds are credited to your balance once confirmed. A confirmation cannot be submitted against an expired order, so if you sent the transfer but missed the window, contact support to have the payment applied. Re-quote and create a fresh order, which your now-funded balance can fund directly.
Step 4: Wait for desk execution
This is the part that makes orders different: there is nothing to call. Your order is now in a queue worked by Zuba’s trading team. An operator fills the order at or better than your quoted rate, and a second operator independently reviews and approves the execution before any funds move. This dual-control step is why orders are not instant. What you can rely on:- An order the desk has not approved never outlives
executionDeadlineAt. If execution is not approved by then, the order automatically fails with reasonreview_expiredand your held funds are returned — you never need to chase an unapproved order. - While the desk works, the order reports
funds_received. Once execution is approved it briefly reportsexecuting; at that point it can no longer be cancelled and proceeds to completion — any post-approval issue is resolved by Zuba operations. - Desk internals (venues, fills, operators) are never exposed on the API. Your contract is the quoted rate and the execution window.
Step 5: Completion
When the desk delivers to your beneficiary, the order landscompleted and the order.completed webhook fires. For orders delivered over a wire rail, the response and webhook carry the outbound wire reference (uetr) for your beneficiary’s bank to trace the payment.
fromAmount at the quoted rate, and the ledger shows the order’s legs against the order id. See Ledger for how to reconcile.
Order lifecycle
Webhooks
Order webhooks share the envelope, signing, and retry behaviour described in Webhook Notifications. Subscribe your endpoint to theorder.* events:
There is no webhook for
executing (desk review is internal) and none for cancelled (cancellation is your own synchronous API call).
Every event for a settlement-funded order carries the settlement block, so you can correlate by reference at any point in the lifecycle; on balance-funded orders settlement is null. Example delivery:
Cancelling an order
You can cancel an order at any point before the desk approves execution:status: "cancelled". While the order is awaiting_funds nothing is held, so cancellation simply closes it; after funds_received the held funds are returned to your balance in full. Retrying a successful cancel returns the already-cancelled order.
Once execution has been approved (executing, or a terminal status other than cancelled), cancel returns 409 ORDER_NOT_CANCELLABLE. Because approval can happen at any moment while the order is funds_received, treat cancellation as a race you can lose: a 409 means the trade is executing and will complete at the quoted rate.
Failures and refunds
A failed order is terminal and automatically refunds the full held amount to your balance; there is no partial execution and nothing to claim. (The desk can also reject an order that is stillawaiting_funds — nothing was held, so there is nothing to refund.) failureReason carries a fixed taxonomy:
A
failed order is a normal, fully-resolved outcome, not a stuck state:
the terminal status and the refund arrive together. Refunds always land on
your Zuba balance, never as an outbound transfer back to you; withdraw them
with a payout if you need them off-platform. Reconcile by matching the hold
and its reversal against the order id in your
ledger history.expired order held nothing, so there is no refund; any transfer that arrives after expiry is credited to your balance.
Listing orders
GET /v1/orders returns your orders newest-first with cursor pagination, filterable by status and a from/to creation-time window:
clientRef values per order; they surface on the order, the webhooks, and your ledger history, and make reconciliation trivial.
Testing in the sandbox
The sandbox exercises the real order machinery end to end. Two mechanisms cover the parts that involve other actors in production: Deterministic desk outcomes. Orders are executed manually in production, so two magic sell amounts drive a funded order through the real desk workflow to a terminal state without a human operator. Any other amount stays infunds_received awaiting manual execution, exactly as in production.
The match is on the order’s source (sell) amount, is numeric (
1111.11 matches 1111.1100), and works in any sell currency.
Simulated settlement funding. The sandbox deposit endpoint accepts a narration field; include an order’s settlement reference in it to exercise the real automatic matcher.
A complete settlement-funded run, using a fiat sell currency (settlements in crypto sell currencies fund through your own deposit addresses by exact amount, so the narration mechanism below does not apply to them):
- Quote with the sell side pinned to a magic amount:
{"intent": "order", "fromCurrency": "USD", "toCurrency": "NGN", "fromAmount": "1111.11"}. - Create the order with the returned quote id and a beneficiary holding an active account in the buy currency. With no balance in the sell currency, the order lands
awaiting_fundsand returns thesettlementblock (order.created+order.awaiting_fundsfire). - Simulate your transfer:
POST /v1/sandbox/depositswithamountset torequiredAmount,currencyset to the sell currency, andnarrationcontaining the settlementreference. The matcher attributes it, holds the funds, and firesorder.funds_received. - The magic sell amount then drives the desk flow:
order.completed(ororder.failedfor2222.22) arrives shortly after. Do not assume a fixed delay; wait on the webhook or poll.
funds_received. To test cancellation, use a non-magic amount (the order will sit in awaiting_funds or funds_received) and call the cancel endpoint. To test expiry, create a settlement-funded order and let the funding window lapse. See Sandbox Testing for the full sandbox reference.
Error reference
Next steps
- Webhook Notifications for endpoint setup, signature verification, and retries
- Sandbox Testing for the full deterministic test-value reference
- Ledger for reconciling order holds, settlements, and refunds
- Orders API reference for full request and response schemas