A webhook is an HTTP request a service sends to your server when something happens: a payment succeeds, a pull request opens, an order ships. Instead of polling an API every minute, you get notified. Webhooks are simple, but three things trip teams up: proving the request is genuine, surviving retries, and responding fast enough.
1. Verify the signature
Your webhook endpoint is a public URL. Anyone who finds it can send fake events ("payment succeeded!") unless you check that each request really came from the provider. Most providers sign the payload with an HMAC using a secret only you and they know, and put the result in a header.
The details differ by provider (header name, whether a timestamp is included, hex or Base64 encoding), so always follow their docs. The general pattern is:
- Read the raw request body as bytes. Don't parse and re-serialise JSON first; even whitespace changes break the signature.
- Build the signed string exactly as documented (often
timestamp + "." + body). - Compute HMAC-SHA256 with your webhook secret.
- Compare with the header using a constant-time comparison.
- Reject timestamps older than a few minutes to block replay attacks.
Node.js
import crypto from 'node:crypto';
function verify(rawBody, header, secret, timestamp) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, timestamp: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
msg = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
2. Expect duplicates and out-of-order events
Providers retry when your endpoint times out or returns a non-2xx status, sometimes for days. Networks also deliver the same event twice. Design for at-least-once delivery:
- Store each event ID and skip ones you've already processed (idempotency).
- Don't assume order; re-fetch the latest object state from the API when it matters.
- Make handlers safe to run twice: "set status to paid", not "add 1 to paid count".
3. Respond fast, process later
Return 2xx within a few seconds, then do slow work (emails, PDFs, third-party calls) in a background queue. Long handlers cause timeouts, which cause retries, which cause duplicates.
4. Operate it
- Log the event ID, type and processing result for every delivery.
- Alert when failures spike, and keep a way to replay failed events.
- Rotate webhook secrets periodically. Most providers allow two active secrets during rotation.
- Use the provider's test or CLI tools to send sample events in development.
Useful tools: Hash Generator to sanity-check digests, and JSON Formatter to inspect payloads.