---
title: "Create a buy order (KES in, USDT out)"
method: POST
path: "/v1/orders/buy"
tags: ["Partner API"]
---

# Create a buy order (KES in, USDT out)

`POST /v1/orders/buy`

## Request

- Base URL: `https://sandbox.onlink.africa`
- URL: `https://sandbox.onlink.africa/v1/orders/buy`
- Auth: API key in header `X-OnLink-Key`

Consumes a buy quote and debits the KES directly from your own VA balance — there is nothing to send and no payment instructions to read. Delivery is asynchronous: nothing is sent until the debit is confirmed and the order reaches a terminal state. Register a webhook endpoint rather than assuming a create means the trade is done. Retry-safe: creating twice with the same partnerReference returns the original order rather than a second one.

## Request body

- CreateBuyOrderDto
  - `quoteId` string, required: A quote id from POST /v1/quotes with side=buy. Single-use, and the amounts are taken from it — they are not restated here.
  - `partnerReference` string, required: Your own reconciliation handle. Unique per partner, and RETRY-SAFE: creating twice with the same reference returns the original order rather than a second one.
  - `expectedUsdtAmount` string: Optional. If supplied it must equal the quote’s usdtAmount exactly, or the order is refused (409). This catches a bug on your side where one number is displayed and a different one is ordered.

## Response `202`

Order created; your KES debit has been attempted and is awaiting the bank’s confirmation.

- BuyOrderResponseDto
  - `orderId` string, uuid, required: The order id. Use it for GET /v1/orders/{id}.
  - `status` 'awaiting_payment' | 'awaiting_usdt' | 'payment_matched' | 'usdt_received' | 'funds_reserved' | 'awaiting_approval' | 'rejected' | 'sending' | 'paying_out' | 'settled' | 'expired' | 'review', required: Where the order is now. A newly created buy order is `funds_reserved` — your KES debit has been attempted and is awaiting the bank’s confirmation. `settled`, `rejected` and `expired` are the terminal states. **Delivery is asynchronous — subscribe to webhooks rather than polling this field to completion.**
  - `kesAmount` string, required: The trade PRINCIPAL in KES, decimal string, always 2 decimal places (KES cents). Taken from the quote — never restated by you, never a JSON number. **This is not what was debited**: the debit is `kesTotalAmount`, which adds the disclosed fee.
  - `usdtAmount` string, required: USDT you will receive, decimal string, always 6 decimal places. Never a JSON number. Derived from the PRINCIPAL — the fee does not change it.
  - `rate` string, required: KES per USDT the order is priced at, from the consumed quote.
  - `kesTotalAmount` string, required: **What was actually debited: principal + fee.** This is the figure that appears on your statement, and the one to reconcile against. It equals `kesAmount` exactly when no fee applies.
  - `fee` QuoteFeeDto, required
    - `kesAmount` string, required: The fee charged on this trade, KES, always 2 decimal places. On a **buy** it is ADDED to what you pay; on a **sell** it is DEDUCTED from what you receive. `"0.00"` means no fee. Never a JSON number.
    - `currency` 'KES', required: Always `KES`. The fee is charged in KES on both sides.
    - `percentBps` integer, required: The percentage applied, in basis points — `100` is 1.00%. Applied to the `kesAmount` PRINCIPAL, not to the total, and rounded UP to the cent. An integer, not a string: unlike the amounts, it is not money.
    - `flatKesAmount` string, required: The flat component applied, KES, 2 decimal places. Charged on every order regardless of size. `"0.00"` means no flat component.
    - `capKesAmount` string, nullable, required: The ceiling applied to the whole fee, KES, 2 decimal places — or `null` when the fee is UNCAPPED. **`null` and `"0.00"` are different**: `null` means there is no ceiling, `"0.00"` means the fee is capped at nothing and this trade is free.
  - `expiresAt` string, date-time, required: ISO 8601 UTC. Informational only on this leg — nothing expires it.
  - `kesDebit` BuyKesDebitDto, required
    - `status` 'reserved' | 'confirmed' | 'failed' | 'reversal_pending' | 'reversed', required: Where your KES debit stands. New values may be added over time — treat an unrecognised value as "not final yet" and read the order again later. `reserved` — the debit was attempted and is awaiting the bank’s confirmation; the order is `funds_reserved`. `confirmed` — the bank confirmed the debit and `order.funds_confirmed` is sent; the order is `funds_reserved` or later and not `rejected`. `failed` — the order is `rejected` and we hold no record that your KES was taken: the bank reported the debit as failed, or it was never made. If the amount did leave your KES balance for this order, contact us with the `orderId`. `reversal_pending` — the order is `rejected` AFTER the bank confirmed the debit, so your KES was taken and is owed back to your balance; it has not been returned yet. `reversed` — that KES has been returned to your balance.

## Other responses

- `400`: Malformed body, an empty partnerReference, or a malformed expectedUsdtAmount
- `401`: Signature missing or invalid
- `404`: Unknown quoteId
- `409`: Quote expired, already consumed, or of the wrong side; expectedUsdtAmount does not match the quote; partner suspended; partnerReference already names a sell order
- `422`: Over your per-order or rolling daily cap, your KES balance is insufficient for this quote, or — only when your USDT is delivered to your own wallet — the quote’s USDT amount is under 5 USDT (USDT_SEND_BELOW_MINIMUM) or has more than 2 decimal places (USDT_SEND_AMOUNT_PRECISION). The last two are refused before the quote is consumed and before any KES is debited.
- `503`: A platform control, your KES account, the settlement account, or the debit is not ready: SWAPS_DISABLED, MONEY_MOVEMENT_PAUSED, ORDER_DAILY_CAP_RATE_UNAVAILABLE, TREASURY_ACCOUNT_NOT_REGISTERED, TREASURY_ACCOUNT_UNAVAILABLE, PARTNER_VA_NOT_ALLOCATED, PARTNER_VA_ACTIVATING, PARTNER_VA_SUSPENDED, KES_BALANCE_UNREADABLE, KES_DEBIT_FAILED (the quote is consumed — retry needs a new quote and partnerReference) or KES_DEBIT_AMBIGUOUS (do not create a new order; re-send with the same partnerReference to get the existing order). See /docs/errors.

---

[API](https://skmtc.dev/onlink/apis/onlink-partner-api.md) - [All operations](https://skmtc.dev/onlink/apis/onlink-partner-api/llms.txt) - [OpenAPI document](https://skmtc.dev/onlink/apis/onlink-partner-api/revisions/1d1c6a3671f2?raw)
