Webhooks.
Every state change in your account can be delivered to your server as a signed, idempotent webhook. Retries use exponential backoff over up to ten attempts. All events are available via replay from the dashboard.
Event envelope
Every webhook request body is a JSON object with the same five top-level keys. dataholds the event-specific payload. The dashboard's GET /v1/events response is similar but uses createdAt and payload instead — the webhook wire is the snake-cased version.
{
"id": "evt_01HGKM4Z7WQ4X",
"type": "payment.confirmed",
"livemode": true,
"created_at": "2026-05-15T18:23:00.000Z",
"data": {
"paymentId": "pay_9fX0a2E1",
"checkoutId": "chk_2hM1tQ"
}
}For ID-only events like payment.confirmed, fetch the full object via GET /v1/payments/{paymentId} to read amount, chain, token, and transaction hash. Most other events embed the full resource under a named key (data.invoice, data.subscription, data.checkout, data.customer, data.wallet).
Event types
Subscribe to any of the following. Names are stable; new types are additive.
payment.pending— ID-onlypayment.confirmed— ID-onlypayment.failed— ID-only + failureReasonpayment.refunded— full Payment underdata.paymentpayment.reorg_suspected— ID-only (chain reader saw head divergence at the relevant depth; reversible — may transition back to confirmed)payment.reorged— ID-only (settlement no longer canonical)payment.reversed— fired with payment.reorged; treat as authoritative rollback signal. If your business already shipped goods, this is your trigger to recover.refund.initiated·refund.broadcast·refund.confirmedcheckout.created·checkout.succeeded·checkout.expired(last is ID-only)invoice.created·invoice.sent·invoice.paid·invoice.voided·invoice.past_due·invoice.reminder_sentsubscription.created·subscription.paused·subscription.resumed·subscription.canceled·subscription.past_due·subscription.payment_failed·subscription.plan_changed·subscription.trial_endedsubscription.renewed—data = { subscription, invoice, subscriptionId, nextBillingDate }(full subscription + the paid invoice; legacysubscriptionId/nextBillingDatekept additively; only fires on a confirmed renewal payment)customer.created·customer.updated·customer.deleted(last is ID-only)wallet.connected·wallet.verified·wallet.removedproduct.created·product.updatedprice.created·price.updatedallowance.depletedwebhook.endpoint.created·webhook.endpoint.test— endpoint lifecycle (the latter fires from the dashboard's “Send test event” button)
Verifying signatures
Every webhook request includes an X-OpenSettle-Signature header of the form t=<unix>,v1=<hex> where v1 is the HMAC-SHA256 of `${t}.${body}` using the endpoint's signing secret.
import crypto from "node:crypto";
export function verify(body: string, header: string, secret: string) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string]),
);
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) throw new Error("Malformed signature header");
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
const a = Buffer.from(sig, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("Invalid signature");
}
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
throw new Error("Timestamp outside tolerance");
}
return JSON.parse(body);
}Retries
We retry with exponential backoff over up to ten attempts if your endpoint doesn't return a 2xx within 15 seconds. Delays between attempts step up as: 1m, 5m, 15m, 1h, then 6h for every remaining attempt. After the final attempt the delivery is marked failed and is replayable from the dashboard.
Idempotency
Webhook delivery can send the same event twice — for example, if your server took 16 seconds to respond. Each event has a stable id (prefixed evt_) — use it as your dedup key. Storing processed event IDs for 30 days is sufficient.
Dashboard delivery log
The dashboard at /app/webhooks carries a per-endpoint delivery log with:
- Per-delivery status pill (pending / delivering / succeeded / failed / dead), attempt count, HTTP status code, latency in ms, time since attempt.
- Live updates via Server-Sent Events — the log auto-refreshes when a delivery transitions state, no manual reload needed. A green-dot "Live" indicator at the top right reflects the stream connection.
- Status filter (all / pending / delivering / succeeded / failed / dead) for triage.
- Click any row to expand a detail panel with the delivery ID, event ID, next-attempt timestamp, last error, and a response-body preview (capped at a kilobyte).
- One-click Replay on every row — re-enqueues the delivery; idempotency means double-clicking is safe.