Developer ToolsFree generatorPDF, Excel, Word export

Webhook HMAC Signature Verification (Node.js Generator)

A webhook endpoint that accepts any POST can be forged by anyone who finds the URL. This generator writes an Express receiver that checks an HMAC signature over the raw body, rejects old timestamps and answers fast so the sender does not retry.

Fill in

Lowercase. Use the header your sender documents.

Generated code
Download:

server.js

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

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

if (!SECRET) {
  throw new Error("WEBHOOK_SECRET is not set");
}

// Raw body is required: the HMAC is computed over the exact bytes sent
app.post("/webhooks/erp", express.raw({ type: "*/*", limit: "1mb" }), (req, res) => {
  const signature = req.get("x-signature") || "";
  const timestamp = req.get("x-timestamp") || "";

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!timestamp || Number.isNaN(age) || age > TOLERANCE_SECONDS) {
    return res.status(400).send("stale or missing timestamp");
  }

  // Signed content here is timestamp + "." + body; match your sender's format
  const signedContent = Buffer.concat([Buffer.from(timestamp + "."), req.body]);
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(signedContent)
    .digest("hex");

  const given = Buffer.from(signature, "utf8");
  const wanted = Buffer.from(expected, "utf8");
  if (given.length !== wanted.length || !crypto.timingSafeEqual(given, wanted)) {
    return res.status(401).send("invalid signature");
  }

  let event;
  try {
    event = JSON.parse(req.body.toString("utf8"));
  } catch (err) {
    return res.status(400).send("invalid json");
  }

  // Acknowledge fast, then process asynchronously (queue, worker, agent)
  res.status(200).send("ok");
  setImmediate(() => console.log("received event", event.type || "unknown"));
});

app.listen(3000, () => console.log("listening on 3000"));

Senders differ: some sign only the body, some sign timestamp plus body, some send a prefix such as sha256= before the hex digest. Match the sender's documented scheme exactly. Store event IDs to ignore duplicates, because most senders deliver at least once. Requires Node.js and the express package.

Webhook and event design for ERP agents

Optional: discuss which ERP events should trigger an agent.

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

How to use it

Enter your path, the signature and timestamp header names from the sender's documentation, and the algorithm. Store the shared secret in an environment variable, not in code. Copy the file, install express, and run it behind HTTPS. Send a test event from the ERP or integration platform and check that a modified body or an old timestamp gets rejected.

The method behind it

The sender computes an HMAC of the message using a shared secret and puts the result in a header. The receiver recomputes it over the same bytes and compares. Comparing with timingSafeEqual avoids leaking information through response time. Including a timestamp in the signed content and rejecting values outside a window limits replay of captured requests.

  • Use the raw body bytes, never re-serialized JSON
  • Compare equal length buffers with timingSafeEqual
  • Reject timestamps outside your tolerance
  • Return 2xx quickly, process in the background
  • Deduplicate by event ID

ERP and agent notes

Webhooks are a cheaper way to learn about changes than polling the ERP every minute. A verified event can trigger an agent to re-read the record through a read-only connector rather than trusting the payload alone. Treat the payload as a hint, check current state in the ERP, and apply the same role limits you would for any request.

Common mistakes

The most common bug is parsing the body with a JSON middleware first, which changes whitespace so the HMAC never matches. Others compare strings with ===, log the secret, accept requests without a timestamp, or do slow work before responding, which makes the sender time out and send duplicates.

Frequently Asked Questions

Why must I use the raw body for HMAC verification?

The signature covers the exact bytes the sender transmitted. If a framework parses and re-serializes the JSON, spacing or key order can change and the computed HMAC will not match. Capture the raw buffer before any parsing.

What is timingSafeEqual for?

It compares two buffers in constant time so an attacker cannot learn how many leading characters matched from small response time differences. It requires buffers of equal length, so check lengths first.

How do I stop replay attacks on a webhook?

Include a timestamp in the signed data, reject requests outside a short window such as five minutes, and store processed event IDs so a repeated delivery is ignored. Use HTTPS so the request cannot be captured in transit.

What status code should I return?

Return 200 or 204 once the signature is valid and the event is safely queued. Return 401 for a bad signature and 400 for a malformed or stale request. Avoid slow processing before the response, since senders retry on timeouts.

Netray builds event-driven ERP agents on private infrastructure. Reach out if you need webhooks wired safely into your ERP landscape.