OnLink
Concepts

Idempotency and references

partnerReference makes order creation retry-safe. idempotencyKey does the same for transfers. X-OnLink-Delivery makes webhook handling retry-safe.

Three identifiers make this API safe to retry. None is a header you invent — one is a body field you supply, the other is a header we send.

Creating orders: partnerReference

partnerReference is required on every order create. It is your own handle, unique across your orders.

Creating twice with the same partnerReference returns the original order. It does not create a second one, and it is not an error. That is what makes a timeout safe: if you do not know whether your request landed, send it again with the same reference.

POST /v1/orders/sell  { partnerReference: "your-ref-000123", ... }  → 202 order A
POST /v1/orders/sell  { partnerReference: "your-ref-000123", ... }  → 202 order A

Reusing a reference across sides is refused: a reference that already names a sell order cannot be used to create a buy order, and the reverse. That is a 409.

Store the reference and the order id before you move funds

There is no list-orders endpoint. GET /v1/orders/{id} is the only read, so an order id you never recorded cannot be found afterwards by searching.

Write partnerReference and orderId down durably before sending USDT or instructing a payer. If you lose an id, re-POST with the same partnerReference — the response carries the existing order, which is the recovery path.

Use an identifier from your own system: your internal order id, or a UUID you store beside it. Do not use a timestamp or a counter that could repeat.

Creating transfers: idempotencyKey

POST /v1/transfers uses a different field for the same job: idempotencyKey is required on every create, and it works by comparing the whole request rather than by reference alone:

  • The same key with the same request body returns the original transfer. A timeout or a retried request is safe to resend unchanged.
  • The same key with a DIFFERENT request body is refused — 409 TRANSFER_IDEMPOTENCY_CONFLICT. Reusing a key for a materially different instruction is treated as a mistake worth stopping, not as a new transfer. The original transfer still stands; use a new key for the new instruction.
POST /v1/transfers  { idempotencyKey: "trf-2026-09-11-000123", amount: {"currency":"KES","value":"1500.00"}, ... }  → 202 transfer A
POST /v1/transfers  { idempotencyKey: "trf-2026-09-11-000123", amount: {"currency":"KES","value":"1500.00"}, ... }  → 202 transfer A (same)
POST /v1/transfers  { idempotencyKey: "trf-2026-09-11-000123", amount: {"currency":"KES","value":"2000.00"}, ... }  → 409 TRANSFER_IDEMPOTENCY_CONFLICT

partnerReference on a transfer is optional and purely a label echoed back — unlike on an order, it plays no role in retry safety. idempotencyKey is what makes a transfer create retry-safe.

Batching transfers: two idempotency keys, two different jobs

POST /v1/transfers/batch (see Send a batch of transfers) is a loop over the same create behaviour above, behind one confirmation — and it carries two idempotency keys because it has two different things to protect against being done twice:

  • The batch's own idempotencyKey dedupes the SUBMISSION. Retrying POST /v1/transfers/batch with the same key returns the existing batch — its status, its per-item outcomes, its confirmation block — rather than submitting a second batch.
  • Each item's own idempotencyKey dedupes the PAYMENT. Every item inside items is shaped exactly like a single transfer create body, including its own idempotencyKey, checked against the same partner_id, idempotency_key uniqueness a single POST /v1/transfers uses.

Both are required, and neither substitutes for the other. The batch key alone would let a partially executed batch re-pay on retry: if a batch confirmed, started executing, and then your integration retried the create call before seeing the result, only the batch key would protect the request — nothing would stop item 7 (already paid) from being submitted again under a resubmission that skipped the batch-level replay. Because each item carries its own key, a retried batch that reaches the executor a second time skips every item already in a terminal state and drives forward only the ones that never ran — see Send a batch of transfers for the full partial-success model.

Every webhook carries X-OnLink-Delivery, and the same delivery keeps the same id across every retry. Record the ids you have processed. If one arrives twice, acknowledge with a 2xx and do nothing else.

Duplicates are normal. A duplicate means your acknowledgement did not reach us — not that the event happened twice.

Payment attribution is separate

paymentReference and the transaction hash attribute money to an order; the two identifiers above make requests safe to repeat. They solve different problems — see References and attribution.

On this page