Sell USDT, receive KES
Quote, create, send USDT, attach the hash, receive KES. Five steps.
You send USDT; KES arrives in a payout account you registered. Five steps — the extra one is attaching the transaction hash, and it is not optional.
Settlement is asynchronous
A create returns 202, not 201 — the order exists, but nothing has happened
to the money yet. Register a webhook endpoint and treat the
terminal event as the completion signal; GET /v1/orders/{id} is the
fallback.
sequenceDiagram
autonumber
accTitle: Selling USDT and receiving shillings
accDescr: Ten messages between you, OnLink, the Tron network and your payout account. You take a quote and receive a rate. You create a sell order and receive an order id, a deposit address and a send-before deadline. You send USDT on Tron, then attach the transaction hash. OnLink matches the deposit and sends you the funds-confirmed webhook. OnLink pays shillings to your payout account and sends you the settled webhook.
participant You
participant OnLink
participant Tron
participant Bank as Your payout account
You->>OnLink: POST /v1/quotes (side=sell)
OnLink-->>You: quoteId, rate, expiresAt
You->>OnLink: POST /v1/orders/sell (quoteId, partnerReference)
OnLink-->>You: 202 orderId, depositAddress, sendBefore
You->>Tron: send USDT
You->>OnLink: PATCH /v1/orders/{id} (txHash)
OnLink->>OnLink: match deposit
OnLink-->>You: webhook order.funds_confirmed
OnLink->>Bank: pay out KES
OnLink-->>You: webhook order.settled1. Quote
POST /v1/quotes with side: "sell". Single-use, and it expires — the amounts come
from it, so you do not restate them on create.
2. Create the order
#!/usr/bin/env bash
# Usage: ONLINK_KEY_ID=pk_... ONLINK_SECRET=sk_... ./order-create.sh
set -euo pipefail
HOST="https://sandbox.onlink.africa"
METHOD="POST"
PATH_AND_QUERY="/v1/orders/sell"
BODY='{"quoteId":"3f1c9b6e-0d2a-4a71-9c8e-51b2a7f4d380","partnerReference":"your-own-handle-0002","payoutAccountId":"6d4b8f02-9c17-4e35-a8d0-3f5c7b1e9a42"}'
TIMESTAMP="$(python3 -c 'import time; print(int(time.time() * 1000))')"
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
# Five fields, newline-joined. printf, not echo -e: the trailing newline echo
# adds would be signed and the signature would not match.
SIGNING_STRING="$(printf '%s\n%s\n%s\n%s\n%s' \
"$METHOD" "$PATH_AND_QUERY" "$TIMESTAMP" "$NONCE" "$BODY_HASH")"
SIGNATURE="$(printf '%s' "$SIGNING_STRING" \
| openssl dgst -sha256 -hmac "$ONLINK_SECRET" -hex \
| awk '{print $NF}')"
# --data sends the SAME bytes that were hashed. Piping the body through a tool
# that reformats it, or adds a trailing newline, changes the hash and the
# signature stops matching — with a bare 401 and no reason given.
curl -i -X POST "${HOST}${PATH_AND_QUERY}" \
-H "X-OnLink-Key: ${ONLINK_KEY_ID}" \
-H "X-OnLink-Timestamp: ${TIMESTAMP}" \
-H "X-OnLink-Nonce: ${NONCE}" \
-H "X-OnLink-Signature: v1=${SIGNATURE}" \
-H 'Content-Type: application/json' \
--data "$BODY"| Field | Notes |
|---|---|
quoteId | From POST /v1/quotes with side=sell. Single-use. |
partnerReference | Your own handle, unique per partner. Retry-safe — resending returns the original order. |
payoutAccountId | Which registered KES account receives the funds. See GET /v1/payout-accounts. |
Response — 202 Accepted:
{
"orderId": "c4e8a9d1-7f36-4b02-a58c-1e9d3b7f5a24",
"status": "awaiting_usdt",
"kesAmount": "129500.00",
"usdtAmount": "1000.000000",
"rate": "129.5000",
"kesTotalAmount": "127039.20",
"fee": {
"kesAmount": "2460.80",
"currency": "KES",
"percentBps": 190,
"flatKesAmount": "0.30",
"capKesAmount": null
},
"expiresAt": "2026-09-01T12:34:56.000Z",
"depositAddress": "TRXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"chain": "tron",
"sendBefore": "2026-09-01T12:34:56.000Z"
}kesAmount is the trade principal. What lands in your account is
kesTotalAmount — the principal less the disclosed fee, 129500.00 − 2460.80
above — so that is the figure to reconcile your bank statement against. The fee
object shows the exact terms applied to this order; Fees has
the arithmetic.
Note that usdtAmount is unchanged by the fee: the fee is charged in KES, on the
KES side, on both legs.
3. Send the USDT
Send usdtAmount of USDT on Tron (TRC-20) to depositAddress, before
sendBefore.
Your deposit address is the same for every order
It is long-lived and shared across all of your orders, so it cannot tell two of them apart. Sending without completing step 4 leaves us with a deposit we cannot attribute.
4. Attach the transaction hash
PATCH /v1/orders/{orderId}{ "txHash": "a1b2c3d4e5f6..." }This is what attributes your deposit to this order. Case and a leading 0x are
ignored, since those are the same on-chain transaction. One hash attributes exactly
one order — attaching a hash already used by another order is refused.
5. Settle
You will receive:
order.funds_confirmed— your USDT was attributed to this order. On this leg attribution depends on the hash you supplied, so a missing confirmation is actionable by you: check the hash.order.settled— the KES has been sent to your payout account.order.rejected/order.expired— terminal, and the remedies differ. An expiry means quote and send again; a rejection does not.
Errors worth handling
| Status | Meaning |
|---|---|
409 | The quote expired or was already used; the partnerReference already names a buy order; or that hash is already attached to another order. |
422 | The order exceeds your per-order or rolling-24-hour cap. |
See Errors for the full catalogue and References and attribution for how hashes are matched.
Receive money in Kenya
Shillings arriving at the collection account we issue you: which rails reach it, what attributes each payment to an order, and why one rail gives you no reference to match on.
Buy USDT with KES
Debit your own KES balance directly, receive USDT at a registered address. Quote, create, settle — no payer, no payment rail.