Credentials and integrations

Verify webhooks

Authenticate webhook bytes, validate freshness, prevent replay, acknowledge safely, and process events idempotently.

A webhook endpoint is public by design. The payload must prove which provider sent it, because anyone who discovers the URL can POST convincing-looking JSON at it.

Verify the signature over the raw bytes

Verify the signature over the exact raw body using the provider’s current scheme and a protected secret or public key. Most providers send an HMAC in a header:

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest()
  const received = Buffer.from(signatureHeader, 'hex')
  return expected.length === received.length &&
    timingSafeEqual(expected, received)
}

The raw body detail is where implementations break. If your framework parses the JSON first and you re-serialize it, key order or whitespace changes and every signature fails — or you end up verifying bytes the provider never signed. Capture the body before any parser touches it.

timingSafeEqual matters too: a byte-by-byte comparison that returns early leaks the correct signature through response timing.

Freshness and replay

Check timestamp tolerance, store event IDs, and make processing idempotent. A provider can deliver the same event twice or retry after your response times out. Signature verification proves origin and integrity, but only stored event IDs prevent duplicate fulfillment:

INSERT INTO webhook_events (event_id) VALUES ('evt_1PqR8s')
ON CONFLICT DO NOTHING;
-- zero rows -> already processed: acknowledge and stop

Acknowledge, then work

Acknowledge only after durable acceptance, then process asynchronously when work is slow. This reduces provider retries without pretending an in-memory queue can survive a process crash.

Then test the paths that must fail:

# unsigned payload aimed straight at the endpoint
curl -i -X POST https://api.example.com/webhooks/payments \
  -H "Content-Type: application/json" \
  -d '{"type":"payment.succeeded","id":"evt_fake"}'
# HTTP/1.1 401 Unauthorized — and no order was fulfilled

Do not log signatures or complete sensitive payloads. The webhook log needs the event ID and outcome, not the customer data inside the event.

Save results for valid, changed, expired, duplicate, unsigned, and wrong-key payloads. Prove only one side effect occurs and a duplicate delivery returns a safe success response.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →