Home › Guides › Verify a Stripe webhook signature in n8n

How to verify a Stripe webhook signature in n8n

Matching pack: Failed Payment Dunning (for Stripe), n8n pack (US$34), an importable workflow for self-hosted n8n that already does this check, then skips duplicate events and alerts Slack when a subscription payment fails. Read its test report (22 end-to-end checks in a real local n8n 2.43.7, with Slack and email mocked). Free: Stripe webhook signature checker, which runs in your browser.

You pointed a Stripe webhook endpoint at an n8n Webhook node, and now you want to make sure each request really came from Stripe. n8n's generic Webhook node doesn't check the Stripe-Signature header for you. You can do it in one Code node, as long as you give it the raw request body.

The short answer

  1. In the Webhook node, open Options and turn on Raw Body. The exact bytes Stripe sent then arrive as binary data (property data).
  2. Set NODE_FUNCTION_ALLOW_BUILTIN=crypto in n8n's environment and restart, so a Code node can use require('crypto').
  3. In a Code node, compute HMAC-SHA256 of t + "." + rawBody with your endpoint's whsec_ secret, compare it with every v1 value in the header, and reject timestamps that are too old.
  4. Answer Stripe with a 400 when the check fails and a 2xx when it passes, using a Respond to Webhook node.

Why the parsed JSON body doesn't work

Stripe signs the exact bytes of the request body. Without Raw Body, n8n parses the JSON into $json.body. If you turn that back into a string with JSON.stringify, the spacing and escaping are not guaranteed to match what Stripe sent, so the HMAC won't match even with the right secret. Always hash the raw bytes.

How Stripe's signature works

Source: Stripe docs, "Receive Stripe events in your webhook endpoint", sections "Verify webhook signatures manually" and "Prevent replay attacks" (docs.stripe.com/webhooks). Checked 23 Oct 2026.

The Code node

Put this Code node straight after the Webhook node (Webhook node set to respond "Using 'Respond to Webhook' node", Raw Body on). It is the same check our dunning pack uses, with the secret inlined for clarity:

// n8n Code node, "Run Once for All Items", JavaScript.
// Needs NODE_FUNCTION_ALLOW_BUILTIN=crypto on self-hosted n8n.
const crypto = require('crypto');
const SECRET = 'whsec_...';          // this endpoint's signing secret; better: read it from a Set node or env
const TOLERANCE_SECONDS = 300;
const out = [];
for (let i = 0; i < $input.all().length; i++) {
  const item = $input.all()[i];
  let valid = false, reason = '', event = null;
  try {
    const header = (item.json.headers || {})['stripe-signature'] || '';
    const v1 = [];
    let t = null;
    for (const kv of header.split(',')) {
      const [k, v] = kv.split('=', 2).map((s) => (s || '').trim());
      if (k === 't') t = Number(v);
      if (k === 'v1') v1.push(v);
    }
    if (!t || !v1.length) throw new Error('missing t or v1 in Stripe-Signature');
    const raw = await this.helpers.getBinaryDataBuffer(i, 'data');   // the raw bytes, from Raw Body
    const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(raw).digest('hex');
    const exp = Buffer.from(expected, 'hex');
    const ok = v1.some((s) => { const b = Buffer.from(s, 'hex'); return b.length === exp.length && crypto.timingSafeEqual(b, exp); });
    if (!ok) throw new Error('signature mismatch');
    if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SECONDS) throw new Error('timestamp outside tolerance');
    valid = true;
    event = JSON.parse(raw.toString('utf8'));
  } catch (e) { reason = e.message; }
  out.push({ json: { valid, reason, event } });
}
return out;

Follow it with an IF node on {{ $json.valid }}. On false, a Respond to Webhook node returns 400 and the workflow stops. On true, respond 2xx quickly and carry on with $json.event.

Don't hard-code a live secret in a workflow you share or export. Keep it in one place, such as a Set node you don't commit, or an environment variable if your n8n setup allows reading it.

If require('crypto') fails

By default the Code node can't load Node's built-in modules, so require('crypto') throws until crypto is allowed with NODE_FUNCTION_ALLOW_BUILTIN=crypto (for Docker in internal mode, -e NODE_FUNCTION_ALLOW_BUILTIN=crypto) and n8n is restarted. In n8n 2.x, Code nodes run in a task runner. In internal mode (the default when N8N_RUNNERS_MODE isn't set), setting the variable on the n8n process is enough; this is the mode we tested. n8n has deprecated internal mode and recommends external mode for production. In external mode (N8N_RUNNERS_MODE=external, with an n8nio/runners container), the variable on the n8n container isn't what counts: the runners' launcher config file (/etc/n8n-task-runners.json) sets the javascript runner's environment, under env-overrides. Check that file in your runners image before changing it. We haven't tested external mode or n8n Cloud; this is for self-hosted n8n.

Sources: n8n docs, Webhook node, "Raw Body" option (docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/); n8n docs, "Task runners" and Code node environment variables, NODE_FUNCTION_ALLOW_BUILTIN (docs.n8n.io/hosting/configuration/task-runners/). Checked 23 Oct 2026.

Still says "signature mismatch"?

To see which of these it is, paste the payload, header and secret into the free signature checker. It runs in your browser only and sends nothing anywhere.

Skip the wiring

Failed Payment Dunning (for Stripe), n8n pack (US$34) is this check plus the rest of the job: It verifies Stripe's signature, replies straight away, skips duplicate events and posts a Slack alert with the amount, attempt number and next retry. It can also email the customer a friendly notice with Stripe's hosted pay link; that email becomes a "Final notice" only when Stripe has no retry left. A "Recovered" alert is posted when the payment goes through. Self-hosted n8n only. Its tests ran in a real local n8n 2.43.7 with signed sample events; Slack and email were mocked, and live Stripe deliveries were not used. Writing the endpoint in Node or Python instead? Webhooks Done Right (for Stripe) (US$19) teaches Claude Code and Cursor the same rules.

See Failed Payment Dunning (for Stripe), n8n pack · Read its test report