Webhooks
TokenPay notifies your server of state transitions via signed HTTP POST callbacks. Every event uses the same envelope; the `event` field discriminates.
The envelope
Every outbound webhook carries the following JSON body (fields omitted when not applicable):
{
"event": "payment.completed",
"payment_id": "pay_01J7...",
"order_id": "order_001",
"amount": 1000,
"currency": "AUD",
"status": "completed",
"idempotency_key": "evt_01J7...",
"timestamp": "2026-04-19T11:20:00Z",
"auto_released": true,
"failure_reason": null,
"match_details": null
}amount is in minor units (cents, satang, paise, etc. — matching the currency). The idempotency_key is stable across retries of the same event: use it to dedupe.
Event catalog
| Event | When it fires |
|---|---|
| payment.matched | A buy-side counterparty has been matched to this payment; funds are about to move into escrow. |
| payment.completed | Funds have settled and the merchant has been credited. Fulfilment can proceed. |
| payment.failed | The payment could not be completed. `failure_reason` explains why. |
| payment.expired | The payment window elapsed before a counterparty matched. No funds moved. |
| settlement.completed | A merchant settlement has been finalised. Withdrawn funds have left the escrow. |
| settlement.failed | A merchant settlement could not be completed. `failure_reason` explains why. |
| settlement.voided | A previously scheduled settlement was cancelled prior to funds leaving escrow. |
| settlement.adjustment_applied | A manual or automated adjustment (fee correction, chargeback, refund) was posted to a settlement. |
Signature verification
Every delivery carries an X-TokenPay-Signature header — hex-encoded HMAC-SHA256 of METHOD\nPATH\nTIMESTAMP\nBODY using your webhook secret (found under Settings → Webhooks). Verify it before trusting the payload.
import crypto from 'node:crypto';
function verify(req, rawBody) {
const ts = req.headers['x-tokenpay-timestamp'];
const sig = req.headers['x-tokenpay-signature'];
const canonical = ['POST', req.path, ts, rawBody].join('\n');
const expected = crypto
.createHmac('sha256', process.env.TOKENPAY_WEBHOOK_SECRET)
.update(canonical)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Retry semantics
- Any non-2xx response (or timeout after 10 seconds) triggers a retry.
- Retries use exponential backoff with jitter: ~1m, 5m, 15m, 1h, 6h, 24h. Eight attempts over ~72 hours.
- Deliveries are at-least-once. Your handler must be idempotent — the
idempotency_keyon each event is stable across retries. - After eight failures, the event is marked
permanently_failed. You can inspect the audit trail atGET /v1/webhooks/deliveriesand replay manually.
Ordering guarantees
Events are delivered in the order they are generated per payment, but not globally. If you need global ordering, sort by timestamp on receipt. Because of retries, a later-timestamped event may arrive before an earlier one that is still being retried.