Skip to content
Receiver guide

Build a webhook receiver in Node.js with Express

A minimal Express receiver that keeps the raw body for signature verification, acknowledges fast, and does the real work off the request path.

A webhook receiver has three jobs: verify the sender, acknowledge quickly, and do the real work off the request path. Here is a minimal but production-shaped Express handler that does all three.

A minimal receiver

The one detail that trips people up is body parsing. Signature verification runs over the raw request bytes — so mount express.raw() on the webhook route instead of the global express.json(), which would mutate the body before you can verify it.

import express from 'express';
import crypto from 'node:crypto';

const app = express();
const SECRET = process.env.WEBHOOK_SECRET;

// Keep the RAW body — signatures are computed over the exact bytes,
// not the parsed JSON. express.raw() gives you a Buffer.
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body, req.get('X-Webhook-Signature'))) {
    return res.status(401).send('bad signature');
  }
  res.status(200).end();            // acknowledge fast
  queue.add(JSON.parse(req.body));  // then process asynchronously
});

Verify the signature safely

Compute the HMAC over the exact bytes you received and compare it to the header with a constant-time comparison — never ===, which leaks timing. Reject with 401 on any mismatch; only return 200 once the signature checks out.

function verify(rawBody, header) {
  if (!header) return false;
  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(rawBody)              // the Buffer, unmodified
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  // Constant-time compare; timingSafeEqual throws on length mismatch.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Acknowledge fast, process async

Return 200 as soon as the payload is verified and stored, then hand the work to a queue or background job. If you do slow work inline the provider may time out and retry, giving you duplicate deliveries.

Once the receiver is live, point the provider’s webhook at a HookWatch endpoint so every delivery is captured with its request and response. When one fails you can inspect exactly what was sent and replay it after the fix — no need to recreate the event by hand.

Get started

Start debugging your webhooks.

Point one endpoint at HookWatch, capture a failure, and replay it once it’s fixed. Free during beta.