Tips > Data, APIs & Webhooks

Secure Webhooks with Header Auth and Rotating Tokens

The default n8n webhook URL is public.

n8n webhook URLs are public, so anyone who finds one can trigger the workflow. Enable Header Authentication with a strong secret token and rotate it periodically. For providers like Stripe, GitHub, and Slack, add a Code node that verifies the HMAC signature using a constant-time comparison, with the Webhook node's Raw Body option enabled so the exact payload bytes are available.

Why secure n8n webhooks?

The default n8n webhook URL is public. Anyone who discovers it can trigger your workflow. Use Header Authentication with strong tokens, and rotate those tokens periodically. For critical webhooks, layer in IP allowlisting or HMAC signature validation.

Real-world example: A CRM vendor sends lead notifications to your webhook. Secure it with a shared secret in a custom header, and rotate the token quarterly.

How do you add Header Authentication to a webhook?

Webhook node configuration:

{
  "authentication": "headerAuth",
  "path": "crm/leads"
}

Create an Header Auth credential in n8n:

Name: CRM Lead Webhook Auth
Header Name: X-Webhook-Secret
Header Value: wh_prod_a8f3k29d4m5n6p7q8r9s0t1u2v3w4x5y

How do you verify an HMAC signature?

For HMAC signature validation (used by Stripe, GitHub, Slack), add a Code node:

// Code node: "Verify HMAC Signature"
const crypto = require('crypto');

const secret = 'whsec_your_signing_secret_here';
const signature = $input.first().json.headers['x-hub-signature-256'];
const payload = $input.first().json.rawBody;

const expected = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(payload)
  .digest('hex');

// Use constant-time comparison to prevent timing attacks
const signatureBuffer = Buffer.from(signature || '', 'utf8');
const expectedBuffer = Buffer.from(expected, 'utf8');

if (signatureBuffer.length !== expectedBuffer.length ||
    !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
  return [{
    json: {
      valid: false,
      error: 'Invalid signature',
      statusCode: 401
    }
  }];
}

return [{
  json: {
    valid: true,
    data: $input.first().json.body,
    statusCode: 200
  }
}];

Warning: Enable Raw Body and store secrets securely. This code requires the Webhook node's Raw Body option to be enabled so that rawBody is available. Never use JSON.stringify() on the parsed body for signature verification -- the stringified output may differ from the original bytes. Never hard-code secrets in Code nodes in production. Use n8n credentials or environment variables ($env.WEBHOOK_SECRET) to store signing keys.

This prevents unauthorized triggers. Rotating tokens quarterly limits the blast radius of a leaked secret.

Related: Set a Unique Encryption Key and Back It Up · Use the HTTP Request Node as a Universal Connector

Showcase builds

19 complete workflows from my own projects, each with its n8n workflow JSON to import. Showcase entries link the file at the end of the article.

See the showcase builds

Keep reading

190 entries grouped by topic, from first workflow to queue mode. Free, no signup.

Browse the encyclopedia

Need it built?

I design, build and run n8n systems for clients. Every engagement starts with a $1,500 diagnostic audit, credited toward the build.

Book an introductory call