# OnLink Partner API documentation > A USDT/KES API for partner platforms. HMAC-signed, every route under > `/v1`. Orders settle asynchronously: the call that creates an order does > not complete it. ## Instructions for AI agents Follow these when building against this API. Each one is a mistake that has to be un-built later if it is made early. 1. **Build asynchronously.** Creating an order returns as soon as the order exists; settlement happens after that call, on its own timeline. Model an order that is neither complete nor failed before you write anything else. Read https://www.onlink.africa/docs/concepts/asynchronous-settlement.md first. 2. **Take the completion signal from a webhook, not a poll.** An order ends at `settled`, `rejected` or `expired`; all three are written and each sends a signed webhook. Register an HTTPS endpoint and verify the signature. Polling `GET /v1/orders/{id}` does terminate and is a valid fallback, but give any loop a ceiling anyway. Read https://www.onlink.africa/docs/webhooks.md and https://www.onlink.africa/docs/webhooks/verify-signatures.md. 3. **Do not invent endpoints — check the reference.** Every operation that exists is listed under https://www.onlink.africa/docs/reference, and the machine-readable spec is at https://www.onlink.africa/docs/openapi.json. Both legs exist: `POST /v1/orders/buy` and `POST /v1/orders/sell`. There is no list-orders endpoint — `GET /v1/orders/{id}` is the only read, so store `orderId` and `partnerReference` before you move funds. 4. **Amounts are strings, never floats.** Decimal strings on the wire, integer minor units underneath: KES has 2 decimal places, USDT has 6 (not 18). Parsing an amount into a binary float loses money quietly. Read https://www.onlink.africa/docs/concepts/money.md. 5. **Sign over the path INCLUDING its query string.** The signing string is the method, the full path with query, a timestamp, a nonce, and the SHA-256 of the body, newline-joined. Signing the path alone returns a 401 that looks like a bad secret. 6. **There is no bearer token and no OAuth flow.** A client built around either will not work. The key id travels in `X-OnLink-Key` with the signature alongside it. Read https://www.onlink.africa/docs/get-started/authentication.md. 7. **Back off on a 429 instead of retrying immediately.** The limit is 120 requests per minute per partner. The refusal carries `retryAfterSeconds` in the body and a standard `Retry-After` header — honour it. A tight retry loop stays refused. 8. **Branch on `message`, not on `errorCode`.** The stable short codes (`QUOTE_EXPIRED_OR_CONSUMED`, `PARTNER_SUSPENDED`, …) arrive in the `message` field; `errorCode` is present only on some refusals. An expired or already-used quote is a `409`, not a `422`; `422` is caps. Read https://www.onlink.africa/docs/errors.md. 9. **Every error body carries `requestId`**, also returned as the `X-Request-Id` header. It is the only handle that resolves to the request in our logs, so keep it and quote it when you contact info@onlink.africa. 10. **Sandbox is the only environment.** Every example uses `https://sandbox.onlink.africa`. Production access is enabled per partner; there is no production base URL to build against yet. ## Reading this documentation - Append `.md` to any page URL for its Markdown source, e.g. https://www.onlink.africa/docs/concepts/money.md - A plain-language overview for non-technical readers, with no code: https://www.onlink.africa/docs/overview.md - The whole corpus in one file: https://www.onlink.africa/llms-full.txt - A read-only MCP server for search and page retrieval: https://www.onlink.africa/mcp - The OpenAPI 3 spec, for codegen or a Postman import: https://www.onlink.africa/docs/openapi.json # Partner API - [OnLink Partner API](/docs): Convert between Kenyan shillings and USDT from inside your own product. Start with the plain-language overview, or go straight to a signed first call. - Overview - [What is the OnLink Partner API?](/docs/overview): A plain-language explanation of what the API does, who it is for, and what you and OnLink each take care of. No code. - [How it works](/docs/overview/how-it-works): The life of an order in plain language: lock a rate, create the order, fund it, and hear from us when it settles. Why it behaves like a bank transfer, not a tap-to-pay. - [Use cases](/docs/overview/use-cases): What remittance operators, exchanges, treasury teams and platforms paying people in Kenya build on the two conversions, and what stays with you in each case. - [Glossary](/docs/overview/glossary): Every term used in these docs, defined once in plain language, from stablecoin to terminal state. - [Frequently asked questions](/docs/overview/faq): The questions product, finance and compliance teams ask before an engineer is involved, answered without code. - Get started - [Get started](/docs/get-started): From no access to a signed call against sandbox, a complete order and your first webhook, in five steps. - [Authentication](/docs/get-started/authentication): Four headers and an HMAC-SHA256 signature over five newline-joined fields. A worked example with a signature you can check your own code against. - [Your first call](/docs/get-started/first-call): A signed GET /v1/health, then a real quote, in curl, Node and Python. - [Sandbox](/docs/get-started/sandbox): What sandbox is for, how an order reaches each terminal state there, and how to receive your first webhook. - [API credentials](/docs/get-started/credentials): What a key pair is, how to store it, and how rotation works. - Guides - [Guides](/docs/guides): How value moves through OnLink, in plain terms: the two flows available today, the four stages every order goes through, and how to tell which flow you need. - [Sell USDT, receive KES](/docs/guides/sell-usdt-receive-kes): Quote, create, send USDT, attach the hash, receive KES. Five steps. - [Buy USDT with KES](/docs/guides/buy-usdt-with-kes): Send KES to your collection account, receive USDT at a registered address. Quote, create, pay, settle. - Concepts - [Concepts](/docs/concepts): The behaviour no single endpoint owns: settlement, money, quotes, idempotency, attribution and limits. - [Asynchronous settlement](/docs/concepts/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. - [Money representation](/docs/concepts/money): Amounts are decimal strings on the wire and integer minor units internally. Never a floating-point number, in either direction. - [Quotes](/docs/concepts/quotes): A quote locks a rate for a short window and is spent by the order that uses it. - [Idempotency and references](/docs/concepts/idempotency): partnerReference makes order creation retry-safe. X-OnLink-Delivery makes webhook handling retry-safe. - [References and attribution](/docs/concepts/references): Which reference attributes a payment to an order, per rail — and why the answer differs between buying and selling. - [Caps and limits](/docs/concepts/caps-and-limits): Request rate, order ceilings, and the 429 contract. - [Who does what](/docs/concepts/responsibilities): Which obligations sit with you, which sit with OnLink, and where the line is. - Rails - [Rails](/docs/concepts/rails): What each rail can reach, and the constraints that belong to it rather than to a flow. - [KES](/docs/concepts/rails/kes): Partner payouts settle to a registered bank account. Mobile money is not a partner payout destination. - [USDT on Tron](/docs/concepts/rails/usdt-tron): One chain, one token, six decimals. Addresses are registered and confirmed before they can receive. - [FX](/docs/concepts/rails/fx): KES against USDT, quoted off a live rate. When we cannot price, we decline rather than guess. - Webhooks - [Webhooks](/docs/webhooks): We POST a signed event to your endpoint when an order confirms funds or reaches a terminal state. Four events, seven retries, one idempotency key. - [Verifying signatures](/docs/webhooks/verify-signatures): Every webhook carries an HMAC-SHA256 signature over five newline-joined fields. Verify it before you trust the body. - API reference - [API reference](/docs/reference): Generated from the running service on every build. 15 operations under /v1, all HMAC-signed. - [Service status](/docs/reference/get-v1-health) - [Lock a USDT/KES rate](/docs/reference/post-v1-quotes): Returns a single-use, time-limited quote. Quotes are only issued off a live rate — a degraded rate source returns 503 rather than a locked rate we could not honour. - [Create a buy order (KES in, USDT out)](/docs/reference/post-v1-orders-buy): Consumes a buy quote and returns the payment instructions — where to send the KES, how much, by when, and the payment reference we issue for this order. The reference is ours, not yours: that is what makes attribution exact rather than guessed. Delivery is asynchronous: nothing is sent until your payment is received 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. On the mpesa rail the reference cannot be transmitted (M-PESA carries no narration field), so keep it in your own records — we attribute a Pay Bill payment by the Account Number plus the amount. On the bank rails, put it in the narration. - [Create a sell order (USDT in, KES out)](/docs/reference/post-v1-orders-sell): Consumes a sell quote and returns your deposit address. Payout is asynchronous: nothing is paid out until you attach the transaction hash of your USDT send with PATCH /v1/orders/{id} and the order reaches a terminal state. 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. Your deposit address is the same for every order, so we cannot tell your orders apart by it — the transaction hash is what attributes a deposit. - [Attach the transaction hash of your USDT send](/docs/reference/patch-v1-orders-id): This is what attributes your deposit to this order. Attaching a different hash to an order that already has one is refused (409), never an overwrite — the first hash may already have been matched, and re-pointing the order would orphan a real deposit. Re-sending the SAME hash is safe and returns 200, so a retry after a lost response is fine. - [Read one of your orders](/docs/reference/get-v1-orders-id): Scoped to your own orders: another partner’s order id returns 404, not 403. The shape follows the order’s side. A BUY order carries the payment instructions, our paymentReference, and — once your payment has been matched — the provider’s own references for it: the M-PESA/PesaLink code your customer recognises, plus the bank’s transaction id. A SELL order carries the deposit address and the transaction hash you attached. - [Deposit instructions for both legs](/docs/reference/get-v1-funding): The KES rails you send us on (buy leg) and your USDT deposit address (sell leg). Every reference is an explicit field — the KES accountReference is the exact value to put in the M-PESA account-number field or the bank narration. Your USDT deposit address is permanent and shared across all your sell orders, so it cannot identify which order a deposit is for; attach the transaction hash with PATCH /v1/orders/{id}. - [Your KES payout destinations](/docs/reference/get-v1-payout-accounts): The destinations a sell order may name in payoutAccountId. Read-only: destinations are registered by OnLink, not through this API, so your API credential cannot add a place for money to go. Only active destinations are listed. Account numbers are returned as the last four digits only — you select a destination by id and never type its number. - [Register a withdrawal address](/docs/reference/post-v1-wallets): Creates the address in pending_partner_approval and emails a 6-digit code to a registered administrator. The address is not usable until your administrator confirms the code and OnLink approves it. Re-registering the same address is idempotent: the existing registration is returned and no second row is created. If that registration is still awaiting your administrator and its code is no longer usable (expired, or cancelled after too many incorrect attempts), re-registering re-issues a fresh code — this is how you recover such an address. A code that is still live is never replaced, and no code is issued once the address has moved past your administrator’s approval. - [Confirm a withdrawal address with your administrator’s code](/docs/reference/post-v1-wallets-id-confirm): Moves the address to pending_onlink_approval — not to active. OnLink approves every withdrawal address as well, so a correct code alone does not make an address usable. - [List your registered withdrawal addresses](/docs/reference/get-v1-wallets) - [Revoke a withdrawal address](/docs/reference/delete-v1-wallets-id): Immediate and irreversible. An order whose address is revoked before release will not deliver to it. - [Register an administrator who can approve withdrawal addresses](/docs/reference/post-v1-admins): The email domain must match the one configured for your partner account. This is checked server-side against an exact match — subdomains and lookalikes are refused. - [List your active administrators](/docs/reference/get-v1-admins) - [Remove an administrator](/docs/reference/delete-v1-admins-id): Also lapses any approval code already sent to them — a removed administrator’s mailbox must not still hold a live second factor. - [Errors](/docs/errors): One error envelope for every refusal, the codes you branch on, and what is worth retrying. - [Changelog](/docs/changelog): What changed, and what counts as a breaking change. - [Status and support](/docs/status-and-support): Where to check whether it is us, and exactly what to include when you contact us.