Every state change in your account can be delivered to your server as a signed, idempotent webhook. Retries use exponential backoff up to 24 hours. All events are available via replay from the dashboard or CLI.
All events share the same envelope. The data field contains the resource shape you'd get from the GET endpoint for the same object.
{
"id": "evt_01HGKM4Z7WQ4X",
"type": "payment.confirmed",
"created": 1728567890,
"livemode": true,
"api_version": "2026-04-01",
"data": {
"object": "payment",
"id": "pay_9fX0a2E1",
"amount": 199.00,
"currency": "USD",
"token": "USDC",
"chain": "base",
"status": "confirmed",
"tx_hash": "0x9a2f…3bc1",
"block": 24822413,
"confirmations": 4,
"customer": "cus_9fX0a2E1",
"fee": 2.985,
"net": 196.015
},
"request": { "id": "req_01HGKM4Z7WQ4X" }
}Every webhook request includes an Opensettle-Signature header with a timestamp and HMAC-SHA256 signature over the raw request body.
import crypto from "node:crypto";
export function verify(body: string, header: string, secret: string) {
const [ts, sig] = header.split(",").map((s) => s.split("=")[1]);
const payload = `${ts}.${body}`;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
throw new Error("Invalid signature");
}
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
throw new Error("Timestamp outside tolerance");
}
return JSON.parse(body);
}We retry with exponential backoff for up to 24 hours if your endpoint doesn't return a 2xx within 15 seconds. The full schedule is: 30s, 2m, 10m, 30m, 1h, 3h, 6h, 12h, 24h. After 24 hours of failures, the event goes to a dead-letter state and is replayable from the dashboard.
Webhook delivery can send the same event twice — for example, if your server took 14 seconds to respond. Use the event id as your dedup key; storing processed event IDs for 30 days is sufficient.