# 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 # OnLink API - [OnLink API](/docs): Move money into and out of Kenya from inside your own product. Start with the plain-language overview, or go straight to a signed first call. - Overview - [What is the OnLink 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, platforms paying people in Kenya and products collecting from many payers build on the API, 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): Money in, money out, and the conversion between them: which guide you need, the four stages every order goes through, and what is true of all of them. - [Receive money in Kenya](/docs/guides/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. - [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): Debit your own KES balance directly, receive USDT at a registered address. Quote, create, settle — no payer, no payment rail. - [Send money from your KES account](/docs/guides/send-money-from-your-kes-account): Create an instruction, your registered person confirms it with a one-time code, then it executes. Six rails, one consent step, one honest failure model. - [Send a batch of transfers](/docs/guides/send-a-batch-of-transfers): Submit N transfers in one call, consent to the whole batch with a single code, and get a per-item outcome. A loop over the single-transfer behaviour behind one confirmation. - [Send USDT from your wallet](/docs/guides/send-usdt-from-your-wallet): Create an instruction, your registered person confirms it with a one-time code, then it executes. One rail, one consent step, one honest failure model. - [Open a collection](/docs/guides/open-a-collection): Mint a reference and a dedicated USD deposit account for one collection, read the fee terms up front, then read the collection back at any time. - [Collect in the US, pay out in Kenya](/docs/guides/collect-in-the-us-pay-out-in-kenya): End to end: take USD from US contributors by ACH, FedNow or wire, then convert and pay out KES to M-PESA, a bank account or PesaLink. - 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. - [Balances](/docs/concepts/balances): The account you hold with us, what the number means, how fresh it is, and why it is not the number to build a spending check on. - [Quotes](/docs/concepts/quotes): A quote locks a rate for a short window and is spent by the order that uses it. - [Fees](/docs/concepts/fees): The fee is a named component beside the rate, not folded into it. The formula, the rounding direction, and which figure lands on your bank statement. Mechanics, never numbers: your rate is in your agreement, not on this page. - [Idempotency and references](/docs/concepts/idempotency): partnerReference makes order creation retry-safe. idempotencyKey does the same for transfers. X-OnLink-Delivery makes webhook handling retry-safe. - [References and attribution](/docs/concepts/references): Which reference identifies which order, per leg — and why the buy leg needs none of them any more. - [Caps and limits](/docs/concepts/caps-and-limits): Request rate, order ceilings, and the 429 contract. - [Products and scopes](/docs/concepts/products-and-scopes): Why an endpoint documented on this site answers 403 for your credential: every key carries an explicit list of what it may call, and there is no wildcard. - [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): Every rail in the Kenyan corridor, what each one reaches, which of them an API call reaches today, and whether your reference survives the journey. - [KES](/docs/concepts/rails/kes): The Kenyan rails, rail by rail and direction by direction: which reach the account we issue you, where a payout can settle, and what each one does to your reference. Partner payouts settle to a registered bank account, never mobile money, and the USDT buy leg debits your own balance rather than using a collection rail. - [Cut-off times](/docs/concepts/rails/cut-offs): Which Kenyan rails observe a banking day and which never close, why we do not publish a clock time, and how to find the one that applies to your account. - [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 money lands in your account, or when an order, a transfer, a USDT send or a collection confirms funds or reaches a terminal state. Twelve 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. 28 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 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. - [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 tells you where your KES debit stands. 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. - [Your balances](/docs/reference/get-v1-balances): The KES account you hold with us and its available balance. USDT is added when the ledger ships. - [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. - [Read a collection](/docs/reference/get-v1-collections-id) - [List your collections](/docs/reference/get-v1-collections) - [Read a batch, with every item outcome](/docs/reference/get-v1-transfers-batch-id) - [Read a transfer](/docs/reference/get-v1-transfers-id) - [Read a USDT send](/docs/reference/get-v1-usdt-sends-id) - [Open a collection](/docs/reference/post-v1-collections): Mints a reference and a dedicated USD deposit account. expectedAmount is advisory — a mismatch is reported later, never refused here. - [Resend the confirmation code](/docs/reference/post-v1-confirmations-id-resend): Re-issues the code on the shared escalating resend ladder. Refused while the previous code is still within its cooldown window. - [Confirm a money-movement instruction](/docs/reference/post-v1-confirmations-id): Verifies the code sent to your registered confirmation contact and, once correct, hands the instruction off for execution. The response reflects the instruction status immediately after this call (confirmed) — poll GET /v1/transfers/:id for what happens next. - [Ask a payer to send you KES over M-PESA](/docs/reference/post-v1-funding-mpesa-push): Sends an STK Push prompt to the number you name. The payer approves it on their own handset with their PIN — there is no OTP for you to handle and no way for you to complete it on their behalf. The deposit lands in your own KES account, which is resolved from your verified business and is not something you can specify. This returns as soon as the prompt is out, so treat the response as pending: the completion signal is the credit.received webhook, and a prompt the payer declines or ignores produces no event at all. There is no idempotency key. A repeated call is a second prompt, and if the payer approves both then both deposits land and each raises its own credit.received. - [Send N transfers under one confirmation](/docs/reference/post-v1-transfers-batch): A loop over POST /v1/transfers behind a single confirmation code (spec §2.3). Each item is validated exactly as a single transfer would be; a failed item is reported with its refusal reason and the rest proceed. Nothing reaches the bank until the one code is verified via POST /v1/confirmations/:id. - [Send money from your KES account](/docs/reference/post-v1-transfers): Validates the destination, verifies the beneficiary name where the rail supports it, checks your caps and balance, then writes the instruction and sends a confirmation code to your registered person. Nothing reaches the bank until that code is verified via POST /v1/confirmations/:id. - [Send USDT from your wallet to a registered destination](/docs/reference/post-v1-usdt-sends): Validates the destination against your wallet allowlist (only an active wallet may receive value), checks the amount against our minimum and your caps, then writes the instruction and sends a confirmation code to your registered person. Nothing reaches the chain until that code is verified via POST /v1/confirmations/:id. - [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.