TokenPay DevelopersMerchant Dashboard

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

EventWhen it fires
payment.matchedA buy-side counterparty has been matched to this payment; funds are about to move into escrow.
payment.completedFunds have settled and the merchant has been credited. Fulfilment can proceed.
payment.failedThe payment could not be completed. `failure_reason` explains why.
payment.expiredThe payment window elapsed before a counterparty matched. No funds moved.
settlement.completedA merchant settlement has been finalised. Withdrawn funds have left the escrow.
settlement.failedA merchant settlement could not be completed. `failure_reason` explains why.
settlement.voidedA previously scheduled settlement was cancelled prior to funds leaving escrow.
settlement.adjustment_appliedA 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_key on each event is stable across retries.
  • After eight failures, the event is marked permanently_failed. You can inspect the audit trail at GET /v1/webhooks/deliveries and 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.