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

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

`POST /v1/orders/sell`

## Request

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

Consumes a sell quote and takes your USDT at create — from your USDT balance, or from your own wallet, depending on your account. Payout is asynchronous: nothing is paid out until the order’s USDT is confirmed. A signed webhook tells you when it settles; polling is the fallback. Retry-safe: creating twice with the same partnerReference returns the original order rather than a second one. The response carries your deposit address, which is the same for every order: it tops up your USDT balance and is not tied to any one order.

## Request body

- CreateSellOrderDto
  - `quoteId` string, required: A quote id from POST /v1/quotes with side=sell. 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.
  - `payoutAccountId` string, required: Which of your registered KES destinations to pay out to. Destinations are set up by OnLink, not through this API — see GET /v1/funding.

## Response `202`

Order created, awaiting your USDT

- SellOrderResponseDto
  - `orderId` string, uuid, required: The order id. Use it for PATCH and 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 sell order is `awaiting_usdt`; attaching the hash moves it on once your deposit is attributed to this order. `settled`, `rejected` and `expired` are the terminal states. **Settlement 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 you receive**: we pay `kesTotalAmount`, which is this figure less the disclosed fee.
  - `usdtAmount` string, required: USDT to send us, decimal string, always 6 decimal places. Never a JSON number.
  - `rate` string, required: KES per USDT the order is priced at, from the consumed quote.
  - `kesTotalAmount` string, required: **What we pay you: principal − fee.** This is the figure that appears on your bank statement, so it is the one to reconcile against.
  - `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: Deposit deadline, ISO 8601 UTC. The same instant as `sendBefore`. This is the deposit window, not the quote TTL — the rate is already locked.
  - `depositAddress` string, required: Your Tron (TRC-20) deposit address, **in full**. It is permanent and the same for every one of your orders; USDT sent to it tops up your USDT balance, and a sell order is paid from that balance — nothing is sent per order. Compare the whole string, never a prefix: a lookalike address differing only in the middle is an irrecoverable burn.
  - `chain` 'tron', required: Deposit network. Tron only at launch.
  - `sendBefore` string, date-time, required: The deposit deadline again, under the name the integration guide uses. The same instant as `expiresAt`; both are present so you need not know they are one field.

## Other responses

- `400`: Malformed body, or an empty partnerReference
- `401`: Signature missing or invalid
- `404`: Unknown quoteId or payoutAccountId
- `409`: Quote expired, already consumed, or of the wrong side; payout destination revoked; partner suspended; partnerReference already names a buy order; LEDGER_SUBJECT_CONFLICT or LEDGER_NOT_APPLICABLE (contact us with the requestId)
- `422`: Over your per-order or rolling daily cap, or your USDT balance is insufficient for this order (INSUFFICIENT_USDT_BALANCE — nothing is debited and the order is rejected), or LEDGER_POST_REJECTED (contact us with the requestId)
- `503`: No USDT deposit address is allocated yet, or a control or account is not ready: ORDER_DAILY_CAP_RATE_UNAVAILABLE, TREASURY_ACCOUNT_NOT_REGISTERED, TREASURY_ACCOUNT_UNAVAILABLE, PARTNER_VA_NOT_ALLOCATED, PARTNER_VA_ACTIVATING, PARTNER_VA_SUSPENDED, CUSTOMER_WALLETS_DISABLED, WALLET_OWNERSHIP_CHECK_FAILED, USDT_RAIL_NOT_CONFIGURED, USDT_COLLECTION_FAILED (refused, nothing collected, order rejected), USDT_COLLECTION_OUTCOME_UNKNOWN (outcome unconfirmed: order held, do not create another or resend USDT), LEDGER_POST_FAILED. 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)
