Tips > Ops & Security

Implement Webhook HMAC Signature Verification

Major platforms (Stripe, GitHub, Shopify, Twilio) sign webhook payloads with HMAC-SHA256 using a shared secret.

Major platforms -- Stripe, GitHub, Shopify, Twilio -- sign webhook payloads with HMAC-SHA256 using a shared secret. Verifying that signature in your workflow proves the request genuinely came from the platform and has not been tampered with. Without verification, anyone who discovers your webhook URL can send forged events.

Real-world example: A Stripe webhook processes subscription cancellations by deactivating user accounts. An attacker discovers the webhook URL and sends forged customer.subscription.deleted events, deactivating legitimate customers.

How do you verify a Stripe webhook signature in n8n?

// Code node: Verify Stripe webhook signature
// Mode: Run Once for All Items
// Place this as the FIRST node after the Webhook node

const crypto = require('crypto');

const webhookSecret = 'whsec_your_stripe_webhook_secret'; // Store in credential
const payload = $input.first().json.rawBody; // Raw body string
const signatureHeader = $input.first().json.headers['stripe-signature'];

function verifyStripeSignature(payload, header, secret) {
  const elements = header.split(',');
  const details = {};

  for (const element of elements) {
    const [key, value] = element.split('=');
    details[key] = value;
  }

  const timestamp = details['t'];
  const signature = details['v1'];

  if (!timestamp || !signature) {
    return { valid: false, error: 'Missing timestamp or signature' };
  }

  // Reject events older than 5 minutes (replay attack protection)
  const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp);
  if (ageSeconds > 300) {
    return { valid: false, error: `Event too old: ${ageSeconds}s` };
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expectedSignature, 'utf8')
  );

  return { valid: isValid, error: isValid ? null : 'Signature mismatch' };
}

const result = verifyStripeSignature(payload, signatureHeader, webhookSecret);

if (!result.valid) {
  // Return error -- workflow stops here for forged requests
  return [{
    json: {
      verified: false,
      error: result.error,
      action: 'rejected',
    }
  }];
}

// Signature valid -- pass the parsed event to downstream nodes
return [{
  json: {
    verified: true,
    event: JSON.parse(payload),
  }
}];

Info: Accessing the Raw Body

Stripe HMAC verification requires the raw request body (before JSON parsing). In the Webhook node, set Options > Raw Body to true to receive the unparsed body string. The raw body is available as $json.rawBody or via $input.first().json.rawBody depending on your Webhook node version.

How do you verify a GitHub webhook signature?

For GitHub webhooks, the signature format is different:

// GitHub uses X-Hub-Signature-256 header with sha256=<hex> format
const crypto = require('crypto');

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

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

const isValid = crypto.timingSafeEqual(
  Buffer.from(signature, 'utf8'),
  Buffer.from(expected, 'utf8')
);

if (!isValid) {
  return [{ json: { error: 'Invalid GitHub signature', rejected: true } }];
}

return [{ json: { verified: true, event: JSON.parse(payload) } }];

What signature format does each platform use?

Platform Header Name Algorithm Format
Stripe stripe-signature HMAC-SHA256 t=timestamp,v1=hex
GitHub x-hub-signature-256 HMAC-SHA256 sha256=hex
Shopify x-shopify-hmac-sha256 HMAC-SHA256 Base64
Twilio x-twilio-signature HMAC-SHA1 Base64
Slack x-slack-signature HMAC-SHA256 v0=hex

Always implement signature verification for production webhook endpoints that trigger sensitive actions.

Related: Use Docker Compose with Health Checks for n8n and PostgreSQL · Use Path Parameters in Webhook URLs for Dynamic Routing

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