Webhook verification

Build a webhook receiver that is secure, idempotent, and observable — so payments update reliably, even under retries and failures.

Core verification steps

  1. Read the raw body bytes. Do not pre‑parse or mutate before verifying.
  2. Validate timestamp. Reject if outside tolerance (e.g., ±5 minutes).
  3. Compute HMAC. expected = HMAC_SHA256(secret, timestamp + '.' + rawBody) and timing‑safe compare.
  4. Secondary read. After accepting, fetch the invoice by id from a trusted API/RPC and confirm status.
  5. Mark order paid only after the trusted read confirms paid.

Suggested headers

X-Webhook-Id: evt_123
X-Webhook-Timestamp: 1733930400
X-Webhook-Signature: t=1733930400,v1=hex(hmac_sha256(secret, t+'.'+rawBody))
X-Webhook-Retry: 3
X-Webhook-Signature-Alg: HMAC-SHA256

Pseudocode — Node.js/Express

app.post('/webhooks/bitcoin', raw({type: '*/*'}), async (req, res) => {
  const t = req.header('x-webhook-timestamp')
  const sig = req.header('x-webhook-signature')
  const body = req.body // Buffer
  const mac = hmacSHA256(SECRET, `${t}.${body}`)
  if (!timingSafeEqual(sig, mac) || Math.abs(now() - t) > 300) return res.sendStatus(400)

  queue(async () => {
    const evt = JSON.parse(body)
    if (evt.type === 'invoice.paid') {
      const inv = await gateway.getInvoice(evt.data.id)
      if (inv.status === 'paid') await markOrderPaid(inv.order_id)
    }
  })
  res.sendStatus(200)
})

Pseudocode — Python/FastAPI

@app.post('/webhooks/bitcoin')
async def hook(request: Request):
  raw = await request.body()
  t = request.headers['x-webhook-timestamp']
  sig = request.headers['x-webhook-signature']
  mac = hmac_sha256(SECRET, f"{t}.{raw}")
  if not timing_safe_equals(sig, mac) or abs(now()-int(t)) > 300:
      return Response(status_code=400)
  enqueue(process_event, raw)
  return Response(status_code=200)

Replay protection

  • Reject old timestamps and cache X-Webhook-Id for a short TTL to block duplicates.
  • Support multiple active secrets during rotation; try newest then old.

Idempotency

  • Upsert by order_id + event_type; ignore if already processed.
  • Make fulfillment safe to repeat (e.g., re‑setting an already paid order is a no‑op).

Retries & backoff

  • Return 200 OK quickly; queue async work to survive restarts.
  • Use exponential backoff with jitter on the sender; cap total retry window.
  • Dead‑letter unprocessed events for manual replay.

Extra hardening

  • Validate TLS; optionally restrict to provider IP ranges.
  • mTLS for high‑security environments (client cert validation).
  • Least‑privilege API keys; rotate secrets regularly.

Observability

  • Log event id, type, delivery attempt, verification result (no PII / no seeds).
  • Track p50/p95 latency and error rate; alert on spikes (see Node monitoring and Bitcoin Flux).
Template: Start from our Integrate invoices via API pattern and plug this verifier into your receiver. See /learn/guides/invoice-api-integration/.