Idempotency and references
partnerReference makes order creation retry-safe. idempotencyKey does the same for transfers, USDT sends and conversions. X-OnLink-Delivery makes webhook handling retry-safe.
Three identifiers make this API safe to retry. Two are body fields you supply —
partnerReference on orders, idempotencyKey on transfers, USDT sends and
conversions — and the third 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 you act on the
order or tell your user it exists. 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.
Creating USDT sends: idempotencyKey
POST /v1/usdt/sends also requires an idempotencyKey. A retried create with
the same key returns the original send rather than starting a second one. Unlike
a transfer, the body is not compared: the original send is returned whatever the
retry carries, so use a new key for every genuinely new send. If sends are
paused when you retry, you get that refusal instead of the original send; retry
later with the same key — never a new one.
Creating conversions: idempotencyKey
POST /v1/conversions (see Convert USD to KES)
requires an idempotencyKey of 8 to 64 characters from A-Z, a-z, 0-9, _
and -. Like a transfer, it compares the request:
- The same key with the same
from,to,amountandpartnerReferencereturns the original conversion — itsidand its currentstatus, which may by then besettledorfailed. Nothing is converted twice, and no rate is read again. - The same key with any of those different is refused —
422IDEMPOTENCY_KEY_REUSED. The original conversion still stands; use a new key for the new conversion.
A retry is checked like a first request before the key is looked up, so if conversions are paused, or your account is suspended, when you retry, you get that refusal instead of the original conversion; retry later with the same key.
POST /v1/conversions { idempotencyKey: "cnv-2026-10-24-0001", amount: "100.00", ... } → 202 conversion C
POST /v1/conversions { idempotencyKey: "cnv-2026-10-24-0001", amount: "100.00", ... } → 202 conversion C (same)
POST /v1/conversions { idempotencyKey: "cnv-2026-10-24-0001", amount: "250.00", ... } → 422 IDEMPOTENCY_KEY_REUSEDBatching 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.
Identifying who paid is separate
A payer's own rail reference — an M-PESA receipt code, a bank's reference — tells you who sent a credit; the 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; how transfers and USD to KES conversions differ. 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 neither leg needs you to attribute a payment any more.