Secure public webhooks in n8n with provider signatures

Learn how to secure n8n public webhooks with provider signatures. Step-by-step guide with examples for Stripe, GitHub, and Slack.
Wavy lines in an abstract design form a smooth, flowing background with a sense of movement and depth.

One of the great things about n8n is that you can expose webhooks directly to the internet. That’s how you integrate payments from Stripe, pushes from GitHub, or messages from Slack into your workflows. But here’s the catch: once a webhook is public, anyone who finds it can send data to it. Without proper security, you risk triggering fake workflows, polluting your data, or even leaking sensitive information.

In this article, I’ll explain how to secure n8n public webhooks using provider signatures. I’ll cover how the system works, why it’s safer than relying on IP whitelists alone, and how to set it up for some of the most common integrations.

Why webhook signatures matter

When a service like Stripe sends a webhook to your n8n instance, it doesn’t just push raw data. It also attaches a signature header. That header proves the request actually came from Stripe and wasn’t forged by someone else.

n8n doesn’t validate these headers by default. It’s up to you to add the logic inside your workflow. Without it, anyone could POST random JSON to your webhook URL and trigger a fake “payment succeeded” event.

How signature verification works

Here’s the basic pattern:

  1. The provider (Stripe, GitHub, Slack etc) creates a hash of the request body using a secret key.
  2. The provider includes the hash in a request header (for example Stripe-Signature).
  3. Your workflow calculates its own hash of the request body using the same secret key.
  4. If the two hashes match, the request is authentic.

This means even if someone copies your webhook URL, they can’t forge valid requests because they don’t know the secret key.

Example: Stripe webhook validation

Stripe is one of the most common n8n integrations where signatures are essential.

  • In your Stripe dashboard, go to Developers > Webhooks.
  • Copy the signing secret for your endpoint.
  • In n8n, set up a webhook node. Add a Function or Code node immediately after it:
const crypto = require('crypto');

// Stripe sends the signature in this header
const signatureHeader = $json["headers"]["stripe-signature"];

// The secret from your Stripe dashboard
const endpointSecret = $env.STRIPE_SIGNING_SECRET;

// Raw body of the request
const rawBody = $json["body_raw"];

// Compute the HMAC
const expectedSignature = crypto
  .createHmac('sha256', endpointSecret)
  .update(rawBody, 'utf8')
  .digest('hex');

if (signatureHeader.includes(expectedSignature)) {
  return [{ verified: true }];
} else {
  throw new Error("Invalid Stripe signature");
}
  • Only continue your workflow if verified is true. Otherwise, stop execution.

This ensures your workflow only processes legitimate Stripe events.

Example: GitHub webhook validation

GitHub also uses HMAC signatures.

  • Set a webhook secret in your GitHub repo settings.
  • GitHub will include a header like X-Hub-Signature-256.
  • In n8n, validate it with code similar to:
const crypto = require('crypto');
const secret = $env.GITHUB_WEBHOOK_SECRET;

const signature = $json["headers"]["x-hub-signature-256"];
const payload = $json["body_raw"];

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

if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
  return [{ verified: true }];
} else {
  throw new Error("Invalid GitHub signature");
}

The timingSafeEqual part is important — it prevents timing attacks that guess the signature.

Example: Slack request signing

Slack uses a timestamp plus a signature.

  • Slack includes X-Slack-Signature and X-Slack-Request-Timestamp.
  • You combine them with the request body and sign with your Slack app secret.
const crypto = require('crypto');

const timestamp = $json["headers"]["x-slack-request-timestamp"];
const sig = $json["headers"]["x-slack-signature"];
const body = $json["body_raw"];
const secret = $env.SLACK_SIGNING_SECRET;

// Build the base string
const basestring = `v0:${timestamp}:${body}`;

const expected = "v0=" + crypto.createHmac("sha256", secret).update(basestring, "utf8").digest("hex");

if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
  return [{ verified: true }];
} else {
  throw new Error("Invalid Slack signature");
}

This protects against replay attacks and forged Slack events.

Production best practices

  • Always use environment variables to store signing secrets. Never hardcode them.
  • Log verification failures for auditing.
  • Use retries with exponential backoff — providers like Stripe will retry failed webhooks automatically.
  • Combine signatures with other protections like firewalls and private networking.
  • Back up your environment files and secrets as part of your disaster recovery plan.

FAQ

Can I rely on IP whitelisting instead?

Not reliably. IP ranges change, and attackers can spoof requests from anywhere. Signatures are far stronger.

Do all providers support signatures?

Most modern APIs do (Stripe, GitHub, Slack, Twilio, SendGrid, etc). Always check the docs.

What if I rotate my signing secret?

Update it in your .env and redeploy. Test immediately after rotation, similar to rotating the encryption key.

Is there an n8n built-in way to verify signatures?

Not yet. It’s up to you to add a Function or Code node. That flexibility is part of why self-hosting n8n on a VPS is so powerful — you get full control.


Securing public webhooks in n8n with provider signatures is one of those small but vital steps. It prevents abuse, keeps your workflows trustworthy, and aligns with best practices for production APIs. Once you set it up, you can sleep a little easier knowing your automation won’t trigger on fake events.