Asynchronous settlement
Orders settle after the call that creates them. Model an order that is neither complete nor failed, and let a webhook tell you when it is.
This is the property of the API that decides how you architect against it, and it is not visible in the endpoint list.
Creating an order does not complete it
POST /v1/orders/sell returns as soon as the order exists. Settlement happens
after that call, on its own timeline.
Your integration must be asynchronous, and it must have somewhere to put an order that is neither complete nor failed.
If a request, a checkout, or a user-facing spinner in your product is blocked on an order reaching its final state, that is the design to change before you write any code.
The shape of an integration that works
Create the order and return control to your caller.
Quote a rate, create the order against that quote, send the USDT, and attach the transaction hash. Then stop. Tell your end user the order is in progress and let your request finish.
Store orderId and partnerReference before you move any funds.
Write both down durably first. There is no list-orders endpoint, so an order id
you never recorded cannot be looked up afterwards. If you do lose one, re-POST
with the same partnerReference: that returns the existing order rather than
creating a second, which is the recovery path.
Let a webhook tell you it finished.
We POST you a signed order.settled, order.rejected or
order.expired when the order reaches a terminal state, and
order.funds_confirmed when your money is attributed. That is the completion
signal. Polling is the fallback, not the design.
Decide what a refusal means in your product before you ship.
rejected is a real outcome, not an edge case: your funds have arrived and the
payout will not happen. There is no self-service reversal endpoint, so the
resolution is a conversation with us. Have a path for it.
The lifecycle, in one picture
stateDiagram-v2
[*] --> awaiting_payment: buy order created
[*] --> awaiting_usdt: sell order created
awaiting_payment --> payment_matched: KES attributed
awaiting_usdt --> usdt_received: deposit attributed
payment_matched --> awaiting_approval
usdt_received --> awaiting_approval
awaiting_approval --> review
review --> awaiting_approval
awaiting_approval --> sending: buy
awaiting_approval --> paying_out: sell
awaiting_approval --> rejected
sending --> settled
paying_out --> settled
awaiting_payment --> expired: unfunded for 24h
awaiting_usdt --> expired: unfunded for 24h
settled --> [*]
rejected --> [*]
expired --> [*]Four of those transitions send you a webhook: reaching payment_matched or
usdt_received sends order.funds_confirmed, and each of the three terminal
states sends its own event. Everything else is silent.
The statuses you will observe
GET /v1/orders/{id} returns the order's status. These are all of them, for
both legs:
status | What it tells you | Terminal |
|---|---|---|
awaiting_payment | Buy order. Your KES payment has not been matched to it. | No |
awaiting_usdt | Sell order. Your on-chain deposit has not been matched to it. | No |
payment_matched | Buy order. Your KES payment is confirmed and attributed. | No |
usdt_received | Sell order. Your deposit is confirmed and attributed. | No |
awaiting_approval | Your funds are confirmed. The order has not yet moved to payout. | No |
review | The order needs attention before it can go further. | No |
sending | Buy order. The USDT transfer is in flight. | No |
paying_out | Sell order. The KES payout is in flight. | No |
settled | The order completed. | Yes |
rejected | The order will not complete. | Yes |
expired | The order was never funded and its window closed. | Yes |
Handle all of them. An integration whose state machine covers only the happy path
strands an order in a status it has no branch for — and awaiting_approval is
the one most often missing from a first implementation, because it is the status
with no equivalent in a synchronous API.
There is no failed status. An order that will not complete is rejected.
Reaching a terminal state
Every order ends at settled, rejected or expired. All three are written, and
each has a webhook.
settled— the value moved. On a sell order the KES payout completed; on a buy order the USDT was sent.rejected— the order will not proceed. Your funds are with us and the remedy is a conversation, not a retry.expired— the order was never funded. You have 24 hours from creation to fund an order on either leg; a sweep expires unfunded orders shortly after that window closes. A funded order never expires — once your money is attributed, expiry is off the table.
review and awaiting_approval are not terminal and send no webhook. An order
can sit in either for a while. That is a pause, not an ending, and the terminal
event still comes.
Polling, if you need it
GET /v1/orders/{id} terminates: poll until status is one of settled,
rejected or expired and you will get there. It is a legitimate fallback for a
missed delivery, or a reconciliation sweep over orders you have not seen a
terminal event for.
It is still the wrong primary design: a webhook reaches you when the transition happens, and a polling interval polite enough to run all day does not. Give any poll loop a ceiling anyway — a bounded number of attempts, then surface the order for attention on your side — so a bug on either side cannot hold a job open indefinitely.
Quotes and orders both expire
A quote is single-use and time-limited: it carries an expiresAt, and presenting
an expired or already-consumed quote when creating an order is a 409 with
QUOTE_EXPIRED_OR_CONSUMED in the message, not a fresh quote. A created sell
order carries a sendBefore — send the USDT before it, or the order is no longer
the one your quote priced.
Read both off the response rather than assuming a window. They are the two timestamps your integration should hold, and neither is a settlement forecast.
Summary
- An order settles after the call that created it. Build asynchronously.
- Model a not-yet-terminal order explicitly;
awaiting_approvalis the status with no synchronous equivalent. - Terminal means
settled,rejectedorexpired— all three are written, and each sends a webhook. - Store
orderIdandpartnerReferencebefore moving funds; there is no way to enumerate orders afterwards. - Reconcile on your own
partnerReference, and treatrejectedas a first-class outcome.