Developer ToolsFree generatorPDF, Excel, Word export

Webhook Signature Verification Code Generator (Node.js)

Anyone who learns your webhook URL can post fake events to it unless you check the signature. This generator produces a working Express receiver that answers the Meta verification handshake and rejects any POST whose HMAC does not match.

Fill in

Generated code
Download:

Express receiver (Node.js)

const express = require('express');
const crypto = require('crypto');

const app = express();

// Keep the raw bytes: the signature is computed over the exact body Meta sent.
app.use('/webhooks/whatsapp', express.raw({ type: 'application/json', limit: '1mb' }));

// 1. Verification handshake (GET)
app.get('/webhooks/whatsapp', function (req, res) {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];
  if (mode === 'subscribe' && token === process.env.WEBHOOK_VERIFY_TOKEN) {
    return res.status(200).send(challenge);
  }
  return res.sendStatus(403);
});

// 2. Event delivery (POST)
app.post('/webhooks/whatsapp', function (req, res) {
  const header = req.get('X-Hub-Signature-256') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.META_APP_SECRET)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  if (!valid) {
    return res.sendStatus(401);
  }

  // Acknowledge fast, process later.
  res.sendStatus(200);
  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch (err) {
    return;
  }
  enqueueEvent(payload);
});

function enqueueEvent(payload) {
  // Push to a durable queue here (database table, Redis stream, SQS, etc.).
  console.log('event received', JSON.stringify(payload).length, 'bytes');
}

app.listen(3000, function () {
  console.log('webhook receiver listening on 3000');
});

Test the signature locally

# Compute a signature for a sample body and send it
BODY='{"object":"whatsapp_business_account","entry":[]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" | sed 's/^.* //')
curl -i -X POST http://localhost:3000/webhooks/whatsapp -H 'Content-Type: application/json' -H "X-Hub-Signature-256: sha256=$SIG" -d "$BODY"

Verify against your platform's current webhook documentation: header names, payload shape and the verification parameters can differ between providers. Always compute the HMAC over the raw body bytes, never over re-serialised JSON. Serve over HTTPS, keep the app secret in a secret store, and return 200 quickly so the sender does not retry.

Webhook review

Optional review of your webhook intake for security and reliability.

Optional. The tool and every export work without this. No spam.

How to use the generated code

Fill in your route, port and the names of the environment variables that hold the app secret and verify token, then copy the code into a Node project with Express installed. Subscribe your webhook in the Meta app dashboard using the public HTTPS URL and the same verify token. Meta will call the GET handler once, then start sending POST events.

  • Install with npm install express.
  • Set the two environment variables before starting the server.
  • Expose the port through HTTPS (reverse proxy or tunnel for testing).
  • Replace the enqueue function with a durable queue write.

How the signature check works

Meta signs the exact request body using your app secret with HMAC SHA256 and sends the hex digest in the X-Hub-Signature-256 header prefixed with sha256=. Your server recomputes the digest over the same raw bytes and compares. The comparison uses timingSafeEqual so response timing does not reveal how many characters matched. If you parse JSON before hashing, whitespace or key order can change and valid events fail.

Operating notes

Respond within a few seconds with a 200 and process asynchronously. Slow handlers cause retries and duplicate events, so make processing idempotent by keying on the message or status ID. Log rejected signatures with the source address but never log the secret. Rotate the app secret on a schedule and after any suspected leak.

Common mistakes

Applying express.json() globally before this route destroys the raw body, so verification always fails. Comparing strings with === is not constant time. Hardcoding the secret in source code and committing it is the most serious error, and it is often found later in a repository scan.

Frequently Asked Questions

Why must I hash the raw body?

The sender signs the exact bytes it transmitted. Parsing and re-serialising JSON can change spacing or key order, producing a different digest. Capture the raw buffer first, verify, and only then parse the JSON.

What is the verify token for?

It is a string you choose that Meta sends back during the GET handshake when you register the webhook. Your server compares it with its stored value and echoes the challenge, proving you control the endpoint.

Does this work for Slack or Teams webhooks?

The pattern is the same idea but the details differ. Slack uses a signing secret with a versioned base string and a timestamp header. This generator targets the Meta style X-Hub-Signature-256 header.

Should I process the event before replying?

No. Verify, reply 200, then process from a queue. Long processing before the reply causes timeouts and repeated deliveries, which then need duplicate handling.

If you would rather have webhook intake, queueing and ERP posting built and monitored for you, Netray can deliver that as part of an integration project.