Docs / Webhooks

Webhooks.

Settle delivers events as POST requests with HMAC-SHA256 signatures over the raw request body. Verify the signature before trusting the payload.

Event shape.

Every event is a JSON object with an id, type, created_at, and a data envelope whose contents depend on the event type.

invoice.paid example
{
  "id": "evt_01J0AHF8X9QY...",
  "type": "invoice.paid",
  "created_at": "2026-04-27T12:00:14Z",
  "data": {
    "invoice": {
      "id": "inv_01HZX9V0K1Q3Y2T7M3N4D5R8S0",
      "amount_usd": 49.00,
      "currency": "USDC",
      "chain": "base",
      "payer": "0x4f3c8...",
      "payment_tx": "0xabc123...",
      "settled_at": "2026-04-27T12:00:14Z",
      "metadata": { "order_id": "ord_123" }
    }
  }
}

Headers.

Every delivery includes the same four Settle headers.

POST /webhooks/settle HTTP/1.1
Content-Type: application/json
settle-signature: 5b9d1e... (hex hmac-sha256 of raw body)
settle-event-id: evt_01J0AHF8X9QY...
settle-attempt: 1
settle-timestamp: 1745758814
  • settle-signature — hex HMAC-SHA256 of the exact request body, using your endpoint’s signing secret as the key.
  • settle-event-id — stable across retries of the same event. Use this for idempotency.
  • settle-attempt — 1-indexed retry counter.
  • settle-timestamp — unix seconds at delivery time.

HMAC verification.

You must verify the signature before parsing or trusting the payload. Use a constant-time comparison — timingSafeEqual in Node, hmac.compare_digest in Python. A naive == on bytes leaks information.

lib/settle.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySettleSignature(opts: {
  rawBody: string;
  signature: string;
  secret: string;
}): boolean {
  const expected = createHmac("sha256", opts.secret)
    .update(opts.rawBody)
    .digest("hex");
  const a = Buffer.from(opts.signature, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

The signature is computed over the exact bytes Settle sent. Read the raw body before any framework middleware parses it as JSON, or the bytes you HMAC will not match. The pattern docs (Next.js, Hono, FastAPI, Express) show the right way to do that for each framework.

Retries.

We expect a 2xx response within 10 seconds. Anything else is treated as a failure and retried with exponential backoff:

AttemptDelay since previous
10 (initial)
21s
32s
44s
58s
doubling, capped at 24h

We keep retrying for up to 72 hours. After that the event moves to a dead-letter queue you can inspect from list_webhook_events and replay individually.

Idempotency.

Use settle-event-id to dedupe. The same event id will appear on every retry of the same delivery. Your handler should record the event id and short-circuit on duplicates — this protects you from double-fulfilling an order if your 200 response was lost on the wire.

Replay.

From the dashboard or via replay_webhook_event(event_id), you can re-deliver any past event as a fresh attempt. This is the easiest way to test a fix in production: deploy, replay the dead-letter event, watch it succeed.

Event list.

Same list as the MCP reference. For most integrations invoice.paid is the only one you need — it fires after the on-chain settlement is finalized at the chain’s configured threshold.

Was this helpful?