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 AReusing 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 —
409TRANSFER_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_CONFLICTpartnerReference 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
idempotencyKeydedupes the SUBMISSION. RetryingPOST /v1/transfers/batchwith 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
idempotencyKeydedupes the PAYMENT. Every item insideitemsis shaped exactly like a single transfer create body, including its ownidempotencyKey, checked against the samepartner_id, idempotency_keyuniqueness a singlePOST /v1/transfersuses.
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.
Handling webhooks: X-OnLink-Delivery
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.
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.
References and attribution
Which reference identifies which order, per leg — and why the buy leg needs none of them any more.