- 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
| Payouts | Orders | |
|---|---|---|
| Execution | Automated, straight-through | Manual, by Zuba’s trade desk |
| Pricing | Quote or auto-priced; rate plus a fixed fee | Always quote-first; a single all-in rate, no separate fee |
| Funding | Your balance, at creation | Your balance, or a per-order transfer against funding instructions |
| Typical size | Everyday amounts, subject to corridor caps | Large transfers, including amounts above payout corridor caps |
| Speed | Seconds to minutes | Within the execution window on the order |
| Cancellation | While still pending | Any time before the desk approves execution |
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 pair is not enabled for orders on your account, or with an amount error if it 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. 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 exactlyrequiredAmountauto-matches; anything else is credited to your balance and attributed manually. - 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. Matching is automatic on referenced bank transfers and crypto deposits; unreferenced bank transfers can take longer when manual attribution is needed.
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. 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:- The order completes or fails by
executionDeadlineAt. If the desk has not executed by then, the order automatically fails with reasonreview_expiredand your held funds are returned. You never need to chase a stuck 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. - 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
| Status | Meaning |
|---|---|
created | The order exists but no funding path has resolved yet. Transient: creation moves straight on to funds_received or awaiting_funds, so polling never observes it (the order.created webhook still fires). |
awaiting_funds | Funding instructions are issued and the order is waiting for your transfer. No funds are held. |
funds_received | The source amount is held and the order is queued for the desk. |
executing | The desk has approved execution. The order can no longer be cancelled. |
completed | Delivered to your beneficiary. uetr is set for wire deliveries. Terminal. |
failed | The desk could not execute, or the execution window lapsed. failureReason is set and the held funds are returned to your balance in full. Terminal. |
expired | The funding window closed before the order was funded. Nothing was held; late-arriving funds credit your balance. Terminal. |
cancelled | You cancelled before execution was approved. Any held funds are returned in full. Terminal. |
Webhooks
Order webhooks share the envelope, signing, and retry behaviour described in Webhook Notifications. Subscribe your endpoint to theorder.* events:
| Event | Fires when |
|---|---|
order.created | The order is created. Arrives together with order.funds_received or order.awaiting_funds. |
order.awaiting_funds | Funding instructions are issued; the payload’s settlement block carries the account or address, amount, and reference. |
order.funds_received | Funds are held and the order is queued for the desk. |
order.completed | The desk delivered; uetr present for wire deliveries. |
order.failed | The order failed; failureReason set, held funds returned. |
order.expired | The funding window closed before the order was funded. |
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 any terminal status), 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 always refunds the full held amount to your balance automatically; there is no partial execution and nothing to claim.failureReason carries a fixed taxonomy:
failureReason | Meaning | What to do |
|---|---|---|
execution_failed | The desk could not execute the order. | Funds are back on your balance. Request a fresh quote and create a new order, or contact support if it recurs. |
review_expired | The execution window lapsed before the desk completed the order. | Same: funds are returned; re-quote and retry. |
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.
| Sell amount | Outcome |
|---|---|
1111.11 | Lands completed, with a sandbox- prefixed uetr. |
2222.22 | Lands failed (execution_failed), with the held funds returned to your balance. |
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
| Status | Code | Cause |
|---|---|---|
| 400 | Idempotency-Key header is required | Missing the required header on create. |
| 400 | QUOTE_NOT_FOUND / QUOTE_EXPIRED / QUOTE_ALREADY_USED / QUOTE_INTENT_MISMATCH | The quote is unknown, past expiresAt, already consumed, or was not minted with intent: "order". Request a fresh order quote. |
| 400 | UNSUPPORTED_CORRIDOR | The currency pair is not enabled for orders on your account. |
| 400 | BENEFICIARY_NOT_FOUND | The beneficiary id does not exist on your account. |
| 400 | NO_SUITABLE_ACCOUNT_FOR_ORDER_CURRENCY | The beneficiary has no active account in the buy currency. |
| 403 | insufficient_scope | Orders are not enabled on your account. |
| 409 | CLIENT_REF_ALREADY_USED | The clientRef was already used on a different order. |
| 409 | ORDER_NOT_CANCELLABLE | Cancel arrived after execution was approved. |
| 422 | INSUFFICIENT_BALANCE | Your balance does not cover fromAmount and settlement funding is not available for the sell currency on your account. |
| 422 | limit exceeded | The order exceeds your per-transaction or monthly limit. Limits are agreed at onboarding; contact your account manager to raise them. |
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