{"openapi":"3.0.0","paths":{"/v1/health":{"get":{"operationId":"PartnerHealthController_health","parameters":[],"responses":{"200":{"description":"Service is reachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerHealthResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Service status","tags":["Partner API"]}},"/v1/quotes":{"post":{"description":"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.","operationId":"PartnerQuotesController_createQuote","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuoteDto"}}}},"responses":{"201":{"description":"Quote locked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponseDto"}}}},"400":{"description":"Both or neither amount supplied, or an amount is malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"503":{"description":"No live rate available — no quote is issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Lock a USDT/KES rate","tags":["Partner API"]}},"/v1/wallets":{"post":{"description":"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.","operationId":"PartnerWalletsController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterWalletDto"}}}},"responses":{"201":{"description":"Address registered, pending approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterWalletResponseDto"}}}},"400":{"description":"Invalid address (format or checksum), chain or asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"409":{"description":"No administrator registered to send the approval code to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Register a withdrawal address","tags":["Partner API"]},"get":{"operationId":"PartnerWalletsController_list","parameters":[],"responses":{"200":{"description":"Your addresses","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PartnerWalletResponseDto"}}}}}},"security":[{"partner-key":[]}],"summary":"List your registered withdrawal addresses","tags":["Partner API"]}},"/v1/wallets/{id}/confirm":{"post":{"description":"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.","operationId":"PartnerWalletsController_confirm","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmWalletDto"}}}},"responses":{"200":{"description":"Confirmed; awaiting OnLink review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmWalletResponseDto"}}}},"400":{"description":"Incorrect code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"403":{"description":"Code expired, cancelled, or too many incorrect attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Confirm a withdrawal address with your administrator’s code","tags":["Partner API"]}},"/v1/wallets/{id}":{"delete":{"description":"Immediate and irreversible. An order whose address is revoked before release will not deliver to it.","operationId":"PartnerWalletsController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked"},"404":{"description":"Not found, or already revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Revoke a withdrawal address","tags":["Partner API"]}},"/v1/admins":{"post":{"description":"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.","operationId":"PartnerWalletsController_addAdmin","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPartnerAdminDto"}}}},"responses":{"201":{"description":"Administrator registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerAdminResponseDto"}}}},"400":{"description":"Email is not on your configured domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Register an administrator who can approve withdrawal addresses","tags":["Partner API"]},"get":{"operationId":"PartnerWalletsController_listAdmins","parameters":[],"responses":{"200":{"description":"Your administrators","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PartnerAdminResponseDto"}}}}}},"security":[{"partner-key":[]}],"summary":"List your active administrators","tags":["Partner API"]}},"/v1/admins/{id}":{"delete":{"description":"Also lapses any approval code already sent to them — a removed administrator’s mailbox must not still hold a live second factor.","operationId":"PartnerWalletsController_removeAdmin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Removed"},"404":{"description":"Not found, or already removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Remove an administrator","tags":["Partner API"]}},"/v1/orders/buy":{"post":{"description":"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.","operationId":"PartnerOrdersController_createBuy","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBuyOrderDto"}}}},"responses":{"202":{"description":"Order created, awaiting your payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuyOrderResponseDto"}}}},"400":{"description":"Malformed body, an empty partnerReference, an unsupported paymentRail, or a malformed expectedUsdtAmount","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"404":{"description":"Unknown quoteId or walletId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"409":{"description":"Quote expired, already consumed, or of the wrong side; expectedUsdtAmount does not match the quote; the address is not active; partner suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"422":{"description":"Over your per-order or rolling daily cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"503":{"description":"Your KES collection account is not allocated or not yet activated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Create a buy order (KES in, USDT out)","tags":["Partner API"]}},"/v1/orders/sell":{"post":{"description":"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.","operationId":"PartnerOrdersController_createSell","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSellOrderDto"}}}},"responses":{"202":{"description":"Order created, awaiting your USDT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SellOrderResponseDto"}}}},"400":{"description":"Malformed body, or an empty partnerReference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"404":{"description":"Unknown quoteId or payoutAccountId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"409":{"description":"Quote expired, already consumed, or of the wrong side; payout destination revoked; partner suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"422":{"description":"Over your per-order or rolling daily cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"503":{"description":"No USDT deposit address is allocated for your account yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Create a sell order (USDT in, KES out)","tags":["Partner API"]}},"/v1/orders/{id}":{"patch":{"description":"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.","operationId":"PartnerOrdersController_attachTxHash","parameters":[{"name":"id","required":true,"in":"path","description":"Your order id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachTxHashDto"}}}},"responses":{"200":{"description":"Hash attached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachTxHashResponseDto"}}}},"400":{"description":"txHash is not a Tron transaction hash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"404":{"description":"Order not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"409":{"description":"A different hash is already attached, the hash already funds another of your orders, the order is not a sell order, or it is no longer awaiting USDT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Attach the transaction hash of your USDT send","tags":["Partner API"]},"get":{"description":"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.","operationId":"PartnerOrdersController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Your order id","schema":{"type":"string"}}],"responses":{"200":{"description":"Your order. The shape follows its side — a sell order matches `OrderDetailResponseDto`, a buy order `BuyOrderDetailResponseDto`. Branch on the presence of `depositAddress` versus `paymentInstructions`, or keep the side you created it with.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OrderDetailResponseDto"},{"$ref":"#/components/schemas/BuyOrderDetailResponseDto"}]}}}},"404":{"description":"Order not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Read one of your orders","tags":["Partner API"]}},"/v1/funding":{"get":{"description":"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}.","operationId":"PartnerFundingController_getFunding","parameters":[],"responses":{"200":{"description":"Deposit instructions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundingResponseDto"}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}},"503":{"description":"No USDT deposit address is allocated for your account yet. This is not self-serve — contact OnLink.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Deposit instructions for both legs","tags":["Partner API"]}},"/v1/payout-accounts":{"get":{"description":"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.","operationId":"PartnerFundingController_listPayoutAccounts","parameters":[],"responses":{"200":{"description":"Your active payout destinations","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PayoutDestinationResponseDto"}}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponseDto"}}}}},"security":[{"partner-key":[]}],"summary":"Your KES payout destinations","tags":["Partner API"]}}},"info":{"title":"OnLink Partner API","description":"USDT/KES partner API. Every request is signed over its raw bytes — see the security scheme. Signing string: METHOD, path (query included), timestamp, nonce and the sha256 of the body, newline-joined.","version":"1.0","contact":{}},"tags":[{"name":"Partner API","description":""}],"servers":[{"url":"https://sandbox.onlink.africa","description":"Sandbox"}],"components":{"securitySchemes":{"partner-key":{"type":"apiKey","in":"header","name":"X-OnLink-Key"}},"schemas":{"PartnerHealthResponseDto":{"type":"object","properties":{"status":{"type":"string","description":"Always the exact string `ok`. Absolute, never hedged: if the plane is not serving you get an HTTP error, not a degraded value in this field.","enum":["ok"],"example":"ok"}},"required":["status"]},"PartnerErrorResponseDto":{"type":"object","properties":{"statusCode":{"type":"number","description":"HTTP status code, repeated in the body.","example":409},"message":{"type":"string","description":"What went wrong. For the refusals this API defines, this is the stable short code to branch on — e.g. `QUOTE_EXPIRED_OR_CONSUMED`, `PAYOUT_ACCOUNT_NOT_PAYABLE`, `TX_HASH_ALREADY_ATTACHED`, `PARTNER_SUSPENDED`. Validation failures instead read `Validation failed on: <fields>`.","example":"QUOTE_EXPIRED_OR_CONSUMED"},"error":{"type":"string","description":"The status name, or the label the thrown exception supplied. Never a stack trace and never an upstream provider’s own error text.","example":"CONFLICT"},"requestId":{"type":"string","description":"Correlation id for this exact request, repeated as the `X-Request-Id` header on this error response. Quote it when asking OnLink about a failure — it is the only handle that resolves to your request in our logs. Note that a SUCCESSFUL response carries neither the header nor a body field: both are written on the error path only. To hold a handle on a call that succeeded, send your own `X-Request-Id`; an inbound value is echoed back here if that request then fails.","example":"req-1756512000000-a1b2c3d"},"errorCode":{"type":"string","description":"Present only when the refusal carries a machine-readable code distinct from `message`. Absent on most partner refusals.","example":"ORDER_EXCEEDS_DAILY_CAP"},"retryAfterSeconds":{"type":"number","description":"Seconds to wait before retrying, mirrored into the standard `Retry-After` header. Set only when a refusal supplies one — and the partner RATE LIMIT does not: that 429 comes from the named `partner` throttler, which carries its window in `Retry-After-partner` (the throttler suffixes the header with its own name) and puts nothing in the body. Read that header for a rate-limit backoff, not this field.","example":30}},"required":["statusCode","message","error","requestId"]},"CreateQuoteDto":{"type":"object","properties":{"side":{"type":"string","enum":["buy","sell"],"description":"buy: you pay KES and receive USDT (on-ramp). sell: you send USDT and receive KES (off-ramp)."},"kesAmount":{"type":"string","description":"KES amount as a decimal string, max 2 decimal places. Provide exactly one of kesAmount or usdtAmount.","example":"10000.00"},"usdtAmount":{"type":"string","description":"USDT amount as a decimal string, max 6 decimal places (Tron USDT precision). Provide exactly one of kesAmount or usdtAmount.","example":"100.000000"}},"required":["side"]},"QuoteResponseDto":{"type":"object","properties":{"quoteId":{"type":"string","description":"The quote id. Single-use: pass it to POST /v1/orders/sell, which consumes it.","format":"uuid","example":"f1c0a5d2-3b4e-4a71-9c8d-0e1f2a3b4c5d"},"side":{"type":"string","description":"Echoes the side you asked for. `buy` prices the on-ramp, `sell` the off-ramp — a sell order will not consume a buy quote (409).","enum":["buy","sell"],"example":"sell"},"kesAmount":{"type":"string","description":"KES leg, decimal string, always 2 decimal places (KES cents). Never a JSON number. Whichever leg you did not supply is the derived one, and it rounds in OnLink’s favour.","example":"1281000.00"},"usdtAmount":{"type":"string","description":"USDT leg, decimal string, always 6 decimal places (Tron USDT precision). Never a JSON number.","example":"10000.000000"},"rate":{"type":"string","description":"KES per USDT at 2 decimal places, the rate this quote is locked at. Quotes are only ever issued off a live rate source.","example":"128.10"},"expiresAt":{"type":"string","description":"When the locked rate stops being executable, ISO 8601 UTC. Short by design — a long-lived locked rate is a free option against us.","format":"date-time","example":"2026-08-31T09:31:30.000Z"},"settlementEstimateSeconds":{"type":"integer","description":"How long settlement is expected to take once your funds are confirmed, in whole seconds. **A different clock from `expiresAt`** — that is how long you have to send, this is how long settlement then takes. One of a small set of published bands rather than a continuously varying figure, so treat it as a band and not as a precise duration. An integer, not a string: unlike the amounts, it is not money.","enum":[1800,21600,86400],"example":21600}},"required":["quoteId","side","kesAmount","usdtAmount","rate","expiresAt","settlementEstimateSeconds"]},"RegisterWalletDto":{"type":"object","properties":{"chain":{"type":"string","enum":["tron"],"description":"Only tron at launch."},"asset":{"type":"string","enum":["USDT"],"description":"Only USDT at launch."},"address":{"type":"string","description":"The withdrawal address. Validated for base58check format AND checksum before it is stored.","example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"label":{"type":"string","description":"Your own label for this address. Never used for routing.","maxLength":120}},"required":["chain","asset","address"]},"RegisterWalletResponseDto":{"type":"object","properties":{"walletId":{"type":"string","description":"The registration id. Use it for POST /v1/wallets/{id}/confirm and DELETE /v1/wallets/{id}.","format":"uuid","example":"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60"},"chain":{"type":"string","description":"Network. Tron only at launch.","enum":["tron"],"example":"tron"},"asset":{"type":"string","description":"Asset. USDT only at launch.","enum":["USDT"],"example":"USDT"},"address":{"type":"string","description":"The withdrawal address, **in full** — exactly as stored, checksum already verified. Compare the entire string when you reconcile: a truncated rendering makes a lookalike address indistinguishable from the real one, and USDT sent to the wrong address is an irrecoverable burn.","example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"label":{"type":"string","description":"Your own label, echoed back. Never used for routing. `null` if you sent none.","nullable":true,"example":"Settlement wallet — Tron"},"status":{"type":"string","description":"Where the address sits in the two-party approval. **`active` is the only deliverable value** — a correct administrator code moves it to `pending_onlink_approval`, and OnLink’s own review is what makes it usable. `revoked` is terminal.","enum":["pending_partner_approval","pending_onlink_approval","active","revoked"],"example":"pending_partner_approval"},"screeningStatus":{"type":"string","description":"Address-screening outcome. Never defaults to a clear verdict: a newly registered address reads `pending` until a screen has actually run.","enum":["pending","clear","flagged"],"example":"pending"},"createdAt":{"type":"string","description":"When the address was registered, ISO 8601 UTC.","format":"date-time","example":"2026-08-31T09:30:00.000Z"},"approvalSentTo":{"type":"string","description":"The administrator mailbox the 6-digit code was sent to, **masked** (`j•••@yourcompany.com`). Masked rather than whole so the response is not an address-harvesting oracle for anyone holding a leaked credential. `null` when no code was issued — an idempotent re-registration of an address that is already past your administrator’s approval.","nullable":true,"example":"o•••@yourcompany.com"},"nextStep":{"type":"string","description":"Plain-language next action, keyed to `status`. Prose for a human reading the response — branch on `status`, not on this string.","example":"A 6-digit approval code was emailed to a registered administrator. Confirm with POST /v1/wallets/{id}/confirm. The address is NOT usable until OnLink also approves it."}},"required":["walletId","chain","asset","address","label","status","screeningStatus","createdAt","approvalSentTo","nextStep"]},"ConfirmWalletDto":{"type":"object","properties":{"otp":{"type":"string","description":"The 6-digit code emailed to your registered administrator. Numeric only.","example":"123456"}},"required":["otp"]},"ConfirmWalletResponseDto":{"type":"object","properties":{"walletId":{"type":"string","description":"The registration id. Use it for POST /v1/wallets/{id}/confirm and DELETE /v1/wallets/{id}.","format":"uuid","example":"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60"},"chain":{"type":"string","description":"Network. Tron only at launch.","enum":["tron"],"example":"tron"},"asset":{"type":"string","description":"Asset. USDT only at launch.","enum":["USDT"],"example":"USDT"},"address":{"type":"string","description":"The withdrawal address, **in full** — exactly as stored, checksum already verified. Compare the entire string when you reconcile: a truncated rendering makes a lookalike address indistinguishable from the real one, and USDT sent to the wrong address is an irrecoverable burn.","example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"label":{"type":"string","description":"Your own label, echoed back. Never used for routing. `null` if you sent none.","nullable":true,"example":"Settlement wallet — Tron"},"status":{"type":"string","description":"Where the address sits in the two-party approval. **`active` is the only deliverable value** — a correct administrator code moves it to `pending_onlink_approval`, and OnLink’s own review is what makes it usable. `revoked` is terminal.","enum":["pending_partner_approval","pending_onlink_approval","active","revoked"],"example":"pending_partner_approval"},"screeningStatus":{"type":"string","description":"Address-screening outcome. Never defaults to a clear verdict: a newly registered address reads `pending` until a screen has actually run.","enum":["pending","clear","flagged"],"example":"pending"},"createdAt":{"type":"string","description":"When the address was registered, ISO 8601 UTC.","format":"date-time","example":"2026-08-31T09:30:00.000Z"},"nextStep":{"type":"string","description":"Plain-language next action. On success this says the address is awaiting OnLink review and is **not yet usable** — a correct code alone never makes an address deliverable.","example":"Approved by your administrator. The address is now awaiting OnLink review and is NOT yet usable for delivery."}},"required":["walletId","chain","asset","address","label","status","screeningStatus","createdAt","nextStep"]},"PartnerWalletResponseDto":{"type":"object","properties":{"walletId":{"type":"string","description":"The registration id. Use it for POST /v1/wallets/{id}/confirm and DELETE /v1/wallets/{id}.","format":"uuid","example":"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60"},"chain":{"type":"string","description":"Network. Tron only at launch.","enum":["tron"],"example":"tron"},"asset":{"type":"string","description":"Asset. USDT only at launch.","enum":["USDT"],"example":"USDT"},"address":{"type":"string","description":"The withdrawal address, **in full** — exactly as stored, checksum already verified. Compare the entire string when you reconcile: a truncated rendering makes a lookalike address indistinguishable from the real one, and USDT sent to the wrong address is an irrecoverable burn.","example":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"},"label":{"type":"string","description":"Your own label, echoed back. Never used for routing. `null` if you sent none.","nullable":true,"example":"Settlement wallet — Tron"},"status":{"type":"string","description":"Where the address sits in the two-party approval. **`active` is the only deliverable value** — a correct administrator code moves it to `pending_onlink_approval`, and OnLink’s own review is what makes it usable. `revoked` is terminal.","enum":["pending_partner_approval","pending_onlink_approval","active","revoked"],"example":"pending_partner_approval"},"screeningStatus":{"type":"string","description":"Address-screening outcome. Never defaults to a clear verdict: a newly registered address reads `pending` until a screen has actually run.","enum":["pending","clear","flagged"],"example":"pending"},"createdAt":{"type":"string","description":"When the address was registered, ISO 8601 UTC.","format":"date-time","example":"2026-08-31T09:30:00.000Z"}},"required":["walletId","chain","asset","address","label","status","screeningStatus","createdAt"]},"AddPartnerAdminDto":{"type":"object","properties":{"email":{"type":"string","description":"Administrator email. MUST be on the domain configured for your partner account — enforced server-side.","example":"ops@yourcompany.com"},"name":{"type":"string","description":"Administrator name, for the audit trail.","maxLength":200}},"required":["email","name"]},"PartnerAdminResponseDto":{"type":"object","properties":{"adminId":{"type":"string","description":"The administrator id. Use it for DELETE /v1/admins/{id}.","format":"uuid","example":"c9f2a3b4-5d6e-4708-9a1b-2c3d4e5f6071"},"email":{"type":"string","description":"Administrator email, returned whole. It is an address you supplied on your own configured domain, so echoing it discloses nothing you do not hold — unlike `approvalSentTo`, which is masked because it accompanies a live second factor.","example":"ops@yourcompany.com"},"name":{"type":"string","description":"Administrator name, as you registered it.","example":"Amina Otieno"},"status":{"type":"string","description":"Lifecycle. Removal is recorded rather than deleted, so a removed administrator reads `removed` and never disappears from the audit trail — but only `active` administrators receive approval codes, and GET /v1/admins lists only those.","enum":["active","removed"],"example":"active"}},"required":["adminId","email","name","status"]},"CreateBuyOrderDto":{"type":"object","properties":{"quoteId":{"type":"string","description":"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":{"type":"string","description":"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.","maxLength":120},"walletId":{"type":"string","description":"Which of your registered withdrawal addresses to deliver the USDT to. The address must already be active — approved by your administrator AND by OnLink. See GET /v1/wallets."},"paymentRail":{"type":"string","description":"How you will send the KES. This is not cosmetic: the rate guarantee is per-rail, and on mpesa the reference cannot be transmitted at all (M-PESA carries no narration), so we attribute your payment by the Account Number plus the amount instead.","enum":["mpesa","pesalink","rtgs","eft"]},"expectedUsdtAmount":{"type":"string","description":"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.","example":"10.000000"}},"required":["quoteId","partnerReference","walletId","paymentRail"]},"BuyPaymentInstructionsDto":{"type":"object","properties":{"rail":{"type":"string","description":"The rail you declared on the order, echoed back.","enum":["mpesa","pesalink","rtgs","eft"],"example":"mpesa"},"paybill":{"type":"string","description":"The M-PESA shortcode. Present on the `mpesa` rail only — absent, not null, on the bank rails.","example":"444174"},"payTo":{"type":"string","description":"**The exact value the payer must quote**, verbatim. On the `mpesa` rail this goes in M-PESA’s \"Account Number\" field; on a bank rail it is the beneficiary account number. This is your own dedicated collection account, which identifies YOU — not this order.","example":"46013001348902"},"beneficiaryName":{"type":"string","description":"The beneficiary name a bank transfer must be addressed to. Included ONLY when the collection account carries a name our bank has confirmed back to us. If it is absent, ask OnLink rather than composing one — the receiving bank matches on this string.","example":"ONLINK MASTER_PESASWAP"},"reference":{"type":"string","description":"**The reference WE issued for THIS order**, identifying the order rather than you. Put it in the narration on a bank rail. On `mpesa` it cannot be transmitted at all (M-PESA carries no narration field), so keep it in your own records and quote it if you need to ask about this order.","example":"OL-7F3K9QB2"},"kesAmount":{"type":"string","description":"KES to send, decimal string, always 2 decimal places (KES cents). Send this amount exactly — never a JSON number, and never rounded.","example":"1281000.00"},"currency":{"type":"string","description":"Always KES on this leg.","enum":["KES"],"example":"KES"},"payBefore":{"type":"string","description":"The payment deadline, ISO 8601 UTC. The same instant as the order’s `expiresAt`.","format":"date-time","example":"2026-09-02T09:30:00.000Z"},"instructions":{"type":"string","description":"Human-readable instructions, consistent with the explicit fields above rather than a substitute for them. Integrate against the fields; show this to a person.","example":"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46013001348902 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order."}},"required":["rail","payTo","reference","kesAmount","currency","payBefore","instructions"]},"BuyOrderResponseDto":{"type":"object","properties":{"orderId":{"type":"string","description":"The order id. Use it for GET /v1/orders/{id}.","format":"uuid","example":"9c1e4b07-6d52-4a83-91fe-40ab72c5d318"},"status":{"type":"string","description":"Where the order is now. A newly created buy order is `awaiting_payment` and moves on once your KES payment is attributed to this order. `settled`, `rejected` and `expired` are the terminal states. **Delivery is asynchronous — subscribe to webhooks rather than polling this field to completion.**","enum":["awaiting_payment","awaiting_usdt","payment_matched","usdt_received","awaiting_approval","rejected","sending","paying_out","settled","expired","review"],"example":"awaiting_payment"},"kesAmount":{"type":"string","description":"KES to send us, decimal string, always 2 decimal places (KES cents). Taken from the quote — never restated by you, never a JSON number.","example":"1281000.00"},"usdtAmount":{"type":"string","description":"USDT you will receive, decimal string, always 6 decimal places. Never a JSON number.","example":"10000.000000"},"rate":{"type":"string","description":"KES per USDT the order is priced at, from the consumed quote.","example":"128.1000"},"expiresAt":{"type":"string","description":"The payment deadline, ISO 8601 UTC. The same instant as `paymentInstructions.payBefore`. This is the payment window, not the quote TTL — the rate is already locked.","format":"date-time","example":"2026-09-02T09:30:00.000Z"},"paymentInstructions":{"description":"Where and how to send the KES.","allOf":[{"$ref":"#/components/schemas/BuyPaymentInstructionsDto"}]},"paymentReference":{"type":"string","description":"The same string as `paymentInstructions.reference`, restated at the top level so a client polling the order need not reach into the instructions for the value it quotes in a support ticket.","example":"OL-7F3K9QB2"}},"required":["orderId","status","kesAmount","usdtAmount","rate","expiresAt","paymentInstructions","paymentReference"]},"CreateSellOrderDto":{"type":"object","properties":{"quoteId":{"type":"string","description":"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":{"type":"string","description":"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.","maxLength":120},"payoutAccountId":{"type":"string","description":"Which of your registered KES destinations to pay out to. Destinations are set up by OnLink, not through this API — see GET /v1/funding."}},"required":["quoteId","partnerReference","payoutAccountId"]},"SellOrderResponseDto":{"type":"object","properties":{"orderId":{"type":"string","description":"The order id. Use it for PATCH and GET /v1/orders/{id}.","format":"uuid","example":"3a7c1f90-8b2d-4e56-9a01-cd23ef456789"},"status":{"type":"string","description":"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.**","enum":["awaiting_payment","awaiting_usdt","payment_matched","usdt_received","awaiting_approval","rejected","sending","paying_out","settled","expired","review"],"example":"awaiting_usdt"},"kesAmount":{"type":"string","description":"KES you will receive, decimal string, always 2 decimal places (KES cents). Taken from the quote — never restated by you, never a JSON number.","example":"1281000.00"},"usdtAmount":{"type":"string","description":"USDT to send us, decimal string, always 6 decimal places. Never a JSON number.","example":"10000.000000"},"rate":{"type":"string","description":"KES per USDT the order is priced at, from the consumed quote.","example":"128.1000"},"expiresAt":{"type":"string","description":"Deposit deadline, ISO 8601 UTC. The same instant as `sendBefore`. This is the deposit window, not the quote TTL — the rate is already locked.","format":"date-time","example":"2026-09-01T09:30:00.000Z"},"depositAddress":{"type":"string","description":"Your Tron (TRC-20) deposit address, **in full** — send USDT here. It is permanent and identical for every one of your orders, so it cannot tell your orders apart: attach the transaction hash with PATCH /v1/orders/{id}. Compare the whole string, never a prefix: a lookalike address differing only in the middle is an irrecoverable burn.","example":"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C"},"chain":{"type":"string","description":"Deposit network. Tron only at launch.","enum":["tron"],"example":"tron"},"sendBefore":{"type":"string","description":"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.","format":"date-time","example":"2026-09-01T09:30:00.000Z"}},"required":["orderId","status","kesAmount","usdtAmount","rate","expiresAt","depositAddress","chain","sendBefore"]},"AttachTxHashDto":{"type":"object","properties":{"txHash":{"type":"string","description":"The Tron transaction hash of your USDT send: 64 hex characters, with or without a 0x prefix. This is what attributes the deposit to this order — we never infer it from the amount, because your deposit address is the same for every order.","example":"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070"}},"required":["txHash"]},"AttachTxHashResponseDto":{"type":"object","properties":{"orderId":{"type":"string","description":"The order the hash is now attached to.","format":"uuid","example":"3a7c1f90-8b2d-4e56-9a01-cd23ef456789"},"status":{"type":"string","description":"The order status after attaching. Still `awaiting_usdt` until our matcher confirms the deposit on-chain — attaching a hash is a claim, not a receipt.","enum":["awaiting_payment","awaiting_usdt","payment_matched","usdt_received","awaiting_approval","rejected","sending","paying_out","settled","expired","review"],"example":"awaiting_usdt"},"txHash":{"type":"string","description":"The hash as stored: lower-cased, `0x` stripped. Re-sending the same hash returns this unchanged (200); a DIFFERENT hash is refused (409), never an overwrite.","example":"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070"}},"required":["orderId","status","txHash"]},"OrderDetailResponseDto":{"type":"object","properties":{"orderId":{"type":"string","description":"The order id. Use it for PATCH and GET /v1/orders/{id}.","format":"uuid","example":"3a7c1f90-8b2d-4e56-9a01-cd23ef456789"},"status":{"type":"string","description":"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.**","enum":["awaiting_payment","awaiting_usdt","payment_matched","usdt_received","awaiting_approval","rejected","sending","paying_out","settled","expired","review"],"example":"awaiting_usdt"},"kesAmount":{"type":"string","description":"KES you will receive, decimal string, always 2 decimal places (KES cents). Taken from the quote — never restated by you, never a JSON number.","example":"1281000.00"},"usdtAmount":{"type":"string","description":"USDT to send us, decimal string, always 6 decimal places. Never a JSON number.","example":"10000.000000"},"rate":{"type":"string","description":"KES per USDT the order is priced at, from the consumed quote.","example":"128.1000"},"expiresAt":{"type":"string","description":"Deposit deadline, ISO 8601 UTC. The same instant as `sendBefore`. This is the deposit window, not the quote TTL — the rate is already locked.","format":"date-time","example":"2026-09-01T09:30:00.000Z"},"depositAddress":{"type":"string","description":"Your Tron (TRC-20) deposit address, **in full** — send USDT here. It is permanent and identical for every one of your orders, so it cannot tell your orders apart: attach the transaction hash with PATCH /v1/orders/{id}. Compare the whole string, never a prefix: a lookalike address differing only in the middle is an irrecoverable burn.","example":"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C"},"chain":{"type":"string","description":"Deposit network. Tron only at launch.","enum":["tron"],"example":"tron"},"sendBefore":{"type":"string","description":"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.","format":"date-time","example":"2026-09-01T09:30:00.000Z"},"txHash":{"type":"string","description":"The Tron transaction hash you attached, normalised to lower-case with any `0x` prefix stripped. `null` until you attach one.","nullable":true,"example":"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070"}},"required":["orderId","status","kesAmount","usdtAmount","rate","expiresAt","depositAddress","chain","sendBefore","txHash"]},"BuyPaymentEvidenceDto":{"type":"object","properties":{"providerReference":{"type":"string","description":"The M-PESA / PesaLink / RTGS / EFT reference for the payment we attributed to this order — the code your own end user recognises. `null` until the payment is matched, or where the rail carried none.","nullable":true,"example":"SJK4Q1T8ZP"},"bankTransactionId":{"type":"string","description":"Our bank’s own transaction id for the same payment, which is what a support conversation about it is keyed on. `null` until the payment is matched.","nullable":true,"example":"CB2026090200001234"}},"required":["providerReference","bankTransactionId"]},"BuyOrderDetailResponseDto":{"type":"object","properties":{"orderId":{"type":"string","description":"The order id. Use it for GET /v1/orders/{id}.","format":"uuid","example":"9c1e4b07-6d52-4a83-91fe-40ab72c5d318"},"status":{"type":"string","description":"Where the order is now. A newly created buy order is `awaiting_payment` and moves on once your KES payment is attributed to this order. `settled`, `rejected` and `expired` are the terminal states. **Delivery is asynchronous — subscribe to webhooks rather than polling this field to completion.**","enum":["awaiting_payment","awaiting_usdt","payment_matched","usdt_received","awaiting_approval","rejected","sending","paying_out","settled","expired","review"],"example":"awaiting_payment"},"kesAmount":{"type":"string","description":"KES to send us, decimal string, always 2 decimal places (KES cents). Taken from the quote — never restated by you, never a JSON number.","example":"1281000.00"},"usdtAmount":{"type":"string","description":"USDT you will receive, decimal string, always 6 decimal places. Never a JSON number.","example":"10000.000000"},"rate":{"type":"string","description":"KES per USDT the order is priced at, from the consumed quote.","example":"128.1000"},"expiresAt":{"type":"string","description":"The payment deadline, ISO 8601 UTC. The same instant as `paymentInstructions.payBefore`. This is the payment window, not the quote TTL — the rate is already locked.","format":"date-time","example":"2026-09-02T09:30:00.000Z"},"paymentInstructions":{"description":"Where and how to send the KES.","allOf":[{"$ref":"#/components/schemas/BuyPaymentInstructionsDto"}]},"paymentReference":{"type":"string","description":"The same string as `paymentInstructions.reference`, restated at the top level so a client polling the order need not reach into the instructions for the value it quotes in a support ticket.","example":"OL-7F3K9QB2"},"payment":{"description":"The provider’s own references for the payment we attributed to this order. Both fields are `null` until it is matched.","allOf":[{"$ref":"#/components/schemas/BuyPaymentEvidenceDto"}]}},"required":["orderId","status","kesAmount","usdtAmount","rate","expiresAt","paymentInstructions","paymentReference","payment"]},"KesFundingRailDto":{"type":"object","properties":{"rail":{"type":"string","description":"Which KES rail these instructions are for.","enum":["mpesa_paybill","bank_transfer"],"example":"mpesa_paybill"},"paybill":{"type":"string","description":"The M-PESA shortcode. Present on the `mpesa_paybill` rail only — absent, not null, on the bank rail.","example":"444174"},"accountReference":{"type":"string","description":"**The exact value the payer must quote**, verbatim. On the paybill rail this goes in M-PESA’s \"Account Number\" field; on the bank rail it is the beneficiary account number. This is your own dedicated collection account at OnLink, which is why reference matching within it is safe.","example":"46013001348902"},"beneficiaryName":{"type":"string","description":"The beneficiary name a bank transfer must be addressed to. Included ONLY when the account carries a name our bank has confirmed back to us. If it is absent, ask OnLink rather than composing one — the receiving bank matches on this string.","example":"ONLINK MASTER_PESASWAP"},"currency":{"type":"string","description":"Always KES on this leg.","enum":["KES"],"example":"KES"},"instructions":{"type":"string","description":"Human-readable instructions, consistent with the explicit fields above rather than a substitute for them. Integrate against the fields; show this to a person.","example":"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46013001348902. Then quote the paymentReference from your order in your own records — we match the payment by the reference we issued, not by the amount."}},"required":["rail","accountReference","currency","instructions"]},"UsdtFundingRailDto":{"type":"object","properties":{"chain":{"type":"string","description":"Deposit network. Tron (TRC-20) only.","enum":["tron"],"example":"tron"},"asset":{"type":"string","description":"Deposit asset. USDT only.","enum":["USDT"],"example":"USDT"},"address":{"type":"string","description":"Your permanent Tron deposit address, **in full**. Identical for every one of your sell orders, so it cannot identify which order a deposit is for — attach the transaction hash with PATCH /v1/orders/{id}. Compare the whole string when you verify it: a lookalike differing only in the middle is an irrecoverable burn.","example":"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C"},"memo":{"type":"string","description":"Always `null`. Stated rather than omitted, because an integrator who sees no `memo` field cannot tell whether we forgot it or Tron has none — and a deposit sent without a REQUIRED memo is lost.","nullable":true,"example":null},"instructions":{"type":"string","description":"Human-readable instructions for the USDT send.","example":"Send USDT on the Tron (TRC-20) network to this address. It is your permanent deposit address and is the same for every sell order, so we cannot tell your orders apart by it — after sending, attach the transaction hash with PATCH /v1/orders/{id}. No memo or tag is used."}},"required":["chain","asset","address","memo","instructions"]},"FundingResponseDto":{"type":"object","properties":{"kes":{"description":"The KES rails you send us on (buy leg). **May be an empty array** — a partner with no activated collection account gets `[]` rather than a placeholder reference, because publishing a reference nothing is watching is money arriving nowhere.","type":"array","items":{"$ref":"#/components/schemas/KesFundingRailDto"}},"usdt":{"description":"Your USDT deposit address (sell leg).","allOf":[{"$ref":"#/components/schemas/UsdtFundingRailDto"}]}},"required":["kes","usdt"]},"PayoutDestinationResponseDto":{"type":"object","properties":{"payoutAccountId":{"type":"string","description":"The id to pass as `payoutAccountId` on POST /v1/orders/sell. Destinations are registered by OnLink out of band, never through this API, so this is a selection rather than an instruction.","format":"uuid","example":"dd9f1e6a-0000-4000-8000-000000000003"},"accountNumberLast4":{"type":"string","description":"The **last four digits only** of the destination account number. There is no route on this API that returns more, by design — you select a destination by id and never type its number.","example":"6789"},"accountName":{"type":"string","description":"The beneficiary name exactly as the receiving bank holds it. Returned whole so you can tell two destinations at the same bank apart.","example":"PESASWAP LIMITED"},"bankCode":{"type":"string","description":"The receiving bank’s code. **`null` means an on-us destination at our own bank, not missing data** — do not render it as an error or a gap.","nullable":true,"example":null},"label":{"type":"string","description":"The label OnLink recorded for this destination, or `null`. Never used for routing.","nullable":true,"example":"Primary settlement"}},"required":["payoutAccountId","accountNumberLast4","accountName","bankCode","label"]}}}}