Core verification steps
- Read the raw body bytes. Do not pre‑parse or mutate before verifying.
- Validate timestamp. Reject if outside tolerance (e.g., ±5 minutes).
- Compute HMAC.
expected = HMAC_SHA256(secret, timestamp + '.' + rawBody)and timing‑safe compare. - Secondary read. After accepting, fetch the invoice by id from a trusted API/RPC and confirm status.
- 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-Idfor 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 OKquickly; 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/.