HookDeploy
Intermediate

Webhook Security Best Practices

Signature verification for Stripe, GitHub, and Shopify. HMAC-SHA256, replay attacks, IP allowlisting, HTTPS, and secret rotation.

HookDeploy Team · May 22, 2026

Webhook endpoints are public URLs. Anyone who discovers yours can POST fake events. This guide covers how to verify that webhooks actually came from the provider you expect.

Always verify signatures

The single most important security step: verify the cryptographic signature on every incoming webhook before processing it.

Providers sign payloads with a shared secret. The signature proves:

  • The payload was not modified in transit
  • The sender knows the secret (i.e., it came from the provider or someone with access)

Never skip verification because “it works in dev.” One unverified endpoint in production is an open door.

HMAC-SHA256 explained

Most providers (Stripe, GitHub, Shopify, Clerk) use HMAC-SHA256:

  1. Provider computes: HMAC-SHA256(secret, raw_request_body)
  2. Provider sends the hash in a header (often prefixed with sha256=)
  3. Your server computes the same HMAC with your copy of the secret
  4. Compare using a timing-safe comparison function
const crypto = require('crypto');

function verifyHmac(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Strip prefix if present (e.g., "sha256=")
  const received = signature.replace(/^sha256=/, '');

  return crypto.timingSafeEqual(
    Buffer.from(received, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Provider-specific verification

Stripe

Header: Stripe-Signature (contains timestamp + signatures)

const event = stripe.webhooks.constructEvent(rawBody, sig, whsec_secret);

Stripe also validates timestamp freshness (rejects events older than 5 minutes) to prevent replay attacks.

GitHub

Header: X-Hub-Signature-256

const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));

Also check X-GitHub-Event matches the event type you expect to handle.

Shopify

Header: X-Shopify-Hmac-Sha256 (base64-encoded HMAC)

const hash = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(hash));

Shopify sends X-Shopify-Topic for the event type and X-Shopify-Shop-Domain for the shop.

Replay attacks

A replay attack re-sends a previously captured valid webhook. Defenses:

Timestamp validation — Stripe embeds a timestamp in the signature and rejects old events. Implement similar checks for other providers:

const MAX_AGE_SECONDS = 300; // 5 minutes
const timestamp = parseInt(sigParts.t, 10);
if (Math.abs(Date.now() / 1000 - timestamp) > MAX_AGE_SECONDS) {
  throw new Error('Timestamp too old');
}

Idempotency keys — track processed event IDs and reject duplicates:

const eventId = event.id; // Stripe evt_xxx, GitHub X-GitHub-Delivery
if (await db.processedEvents.exists(eventId)) {
  return response.status(200).send('Already processed');
}
await db.processedEvents.insert(eventId);

HookDeploy replay header — when replaying from HookDeploy, check for x-hookdeploy-replay: 1 and skip side effects if needed.

IP allowlisting

Some providers publish IP ranges for webhook senders. Allowlisting adds defense-in-depth but is not sufficient alone — IPs can change and spoofing is possible at the TCP level without TLS client certs.

ProviderIP list available
Stripestripe.com/docs/ips
GitHubapi.github.com/meta
Shopifyshopify.dev/docs/apps/build/webhooks/subscribe/https

Use IP allowlisting as a supplement to signature verification, not a replacement.

HTTPS requirements

Every webhook endpoint must use HTTPS. Providers reject HTTP URLs in production configurations.

HookDeploy endpoints are always HTTPS: https://hookdeploy.dev/h/{slug}. When forwarding or replaying to your server, the target URL must also be HTTPS.

For local development, use ngrok or Cloudflare Tunnel — both provide HTTPS URLs that tunnel to localhost.

Secret rotation

Rotate webhook secrets periodically and after any suspected compromise:

  1. Generate a new secret in the provider dashboard
  2. Update your application’s environment variable
  3. Deploy the new secret
  4. Revoke the old secret in the provider dashboard

During rotation, some providers support two active secrets simultaneously. Check your provider’s docs.

HookDeploy-specific security notes

  • Endpoint slugs are not secret — treat them like public URLs
  • Anyone who knows your slug can POST to it — your handler must verify signatures
  • HookDeploy stores payloads encrypted at rest — but limit sensitive data in test webhooks
  • Use role-based access in HookDeploy so only your team sees captured payloads
  • API keys are hashed — rotate if exposed

Checklist

  • Signature verification on every webhook route
  • Raw body preserved for HMAC computation
  • Timing-safe comparison (not ===)
  • Timestamp freshness check where supported
  • Idempotency on event/delivery ID
  • HTTPS only
  • Secrets in environment variables, not code
  • Secret rotation plan documented

Related guides