OnLink
Webhooks

Webhooks

We POST a signed event to your endpoint when an order confirms funds or reaches a terminal state. Four events, seven retries, one idempotency key.

An order changes state after the call that created it, so the completion signal has to reach you rather than be returned to you. Webhooks are that signal: we POST a signed JSON body to an HTTPS endpoint you give us.

Polling GET /v1/orders/{id} still works and still terminates. Use webhooks as the primary signal and polling as the fallback.

Registering your endpoint

Send us the HTTPS URL you want deliveries on and we register it against your partner account, along with the signing secret you will verify with. There is no self-serve subscription endpoint — you do not choose which events you receive, because you receive all four.

Your endpoint must:

  • be HTTPS and publicly reachable;
  • answer within 10 seconds — that is the per-attempt timeout;
  • return any 2xx status to acknowledge. Anything else is a failed attempt;
  • not redirect. We never follow redirects, so a 301 or 302 is a failure, not a hop.

There is no IP allowlist to configure

Deliveries are authenticated by signature, not by source address. We do not publish a fixed set of egress addresses, so do not build a firewall rule around one — verify the signature instead.

The four events

EventWhen it firesTerminal
order.funds_confirmedYour money arrived and was attributed to this order.No
order.settledThe order completed. USDT sent, or KES paid out.Yes
order.rejectedThe order will not complete. Your funds are with us; talk to us.Yes
order.expiredThe order was never funded and its window closed.Yes

Both legs use the same four. On a sell order order.funds_confirmed means your USDT deposit was matched; on a buy order it means your KES payment was. That is deliberate: you write one handler for "my money landed" without caring which leg's internal status carried it.

Not every status change sends an event

Statuses between funds-confirmed and terminal do not produce webhooks, and there is no order-created event — creation is synchronous, so you already have the 202 response. An order can therefore sit quietly for a while between order.funds_confirmed and its terminal event. That is expected, not a missed delivery.

The payload

{
  "id": "0f3c8b21-5d4e-4a97-9c6b-2f81ad0e7c53",
  "type": "order.settled",
  "createdAt": "2026-09-04T09:15:22.481Z",
  "data": {
    "orderId": "c4e8a9d1-7f36-4b02-a58c-1e9d3b7f5a24",
    "partnerReference": "your-ref-000123",
    "side": "sell",
    "status": "settled",
    "kesAmount": "130500.00",
    "usdtAmount": "1000.000000",
    "rate": "130.5000"
  }
}

type is authoritative — branch on it, not on the X-OnLink-Event header, which is a convenience hint. status is the order status that produced the event, so GET /v1/orders/{id} will agree with it.

Amounts are decimal strings, never numbers. See Money for why, and what to do with them.

Ignore fields you do not recognise

We may add fields to data, and we may add new event types. Both are additive changes. A handler that rejects an unknown field or an unknown type will break on a change that is not supposed to break anything — log it and move on.

Headers

HeaderValue
X-OnLink-Signaturev1=<lowercase hex HMAC-SHA256>
X-OnLink-TimestampUnix milliseconds at signing time.
X-OnLink-DeliveryDelivery id. Stable across every retry — your idempotency key.
X-OnLink-EventThe event type, as a hint. type in the body is authoritative.

Verifying signatures has the algorithm and a worked example in Node and Python.

Delivery and retries

The first attempt goes out within about a minute of the transition. If it fails we retry with exponential backoff — 7 attempts in total, spanning roughly 30 minutes — and then stop and dead-letter the delivery.

sequenceDiagram
    autonumber
    participant OnLink
    participant You as Your endpoint

    Note over OnLink: order reaches a new state
    OnLink->>You: POST (attempt 1)
    You--xOnLink: timeout or non-2xx
    OnLink->>You: POST (attempt 2, same X-OnLink-Delivery)
    You--xOnLink: non-2xx
    OnLink->>You: POST (attempt 3, backing off)
    You-->>OnLink: 200
    Note over OnLink,You: delivered — no further attempts

Every attempt carries the same X-OnLink-Delivery id. Use it as an idempotency key: record it, and if you see one twice, acknowledge and do nothing. A duplicate is normal — it means your 2xx did not reach us, not that the event happened twice.

Deliveries are also reconciled: if an event is ever missed, it is queued and sent afterwards, so a missed event arrives late rather than never.

Respond fast, process afterwards

You have 10 seconds. Do not do your settlement work inside the request — write the delivery down, return 2xx, and process it on your own schedule. A handler that calls your ledger, your bank and your email provider before responding will eventually exceed the timeout, and we will retry an event you already handled.

If a delivery dead-letters

Seven failed attempts stop the delivery permanently. Nothing is lost — the order is still readable with GET /v1/orders/{id}, and its status carries the same information the event would have. Reconcile any order you have not seen a terminal event for; that is the safety net worth building, and it is why you should store orderId and partnerReference before you move funds.

On this page