Debug your next webhook with Probe.
All articles
#security#webhooks#cryptography#architecture·6 min read

Webhook Signature Verification: Production Security Guide & Best Practices

A comprehensive guide to verifying webhook cryptographic signatures, mitigating timing attacks, preventing replay exploits, and managing secret rotation.

Probe

Probe Team

Published September 16, 2026

Webhooks pass critical transactional events across public network boundaries. Without cryptographic signature verification, any internet user who discovers your webhook endpoint URL can send forged HTTP requests, potentially triggering unauthorized financial credits, fake database mutations, or privilege escalations.

RFC 2104 defines Hash-based Message Authentication Codes (HMAC) as the standard mechanism for verifying both data integrity and data authenticity. However, improper implementation—such as using standard string comparison operators or verifying signatures on deserialized JSON—leaves systems vulnerable to timing attacks and verification failures.

Comparison of Webhook Signature Implementations

Different third-party providers use different hashing algorithms, header formats, and encoding conventions.

Webhook Signature Schemes Across Leading ProvidersLollipop chart showing security features by provider: Stripe includes timestamp and HMAC-SHA256; GitHub uses HMAC-SHA256 hex; Shopify uses HMAC-SHA256 Base64; Svix uses timestamped HMAC-SHA256 or Ed25519.StripeHMAC-SHA256 + Timestamp (t=, v1=)GitHubX-Hub-Signature-256 (Hex)ShopifyX-Shopify-Hmac-Sha256 (Base64)Standard / Svixwebhook-id + webhook-timestamp + v1

Reference: RFC 2104 & Provider Security Specifications (2025-2026)

Architectural characteristics of webhook signatures across leading modern APIs.

Anatomy of a Secure Webhook Verification Flow

To achieve enterprise-grade security, signature verification must adhere to a strict sequence:

  1. Extract Headers: Retrieve signature and timestamp headers. Reject requests missing these headers with HTTP 401.
  2. Verify Freshness: Compare header timestamp against Date.now(). If the difference exceeds tolerance (e.g. 300 seconds), reject with HTTP 403.
  3. Preserve Raw Bytes: Read raw request body directly from the stream before parsing JSON.
  4. Compute Cryptographic Hash: Compute HMAC-SHA256(secret, timestamp + "." + rawBody).
  5. Constant-Time Comparison: Compare expected digest against incoming header using timingSafeEqual.
  6. Idempotency Check: Check database for duplicate event IDs.
  7. Process Body: Parse JSON and execute application logic.

Preventing Timing Attacks with timingSafeEqual

Standard equality operators in JavaScript (=== or ==) compare strings character-by-character and terminate evaluation upon encountering the first non-matching byte.

In statistical network telemetry, an attacker can send thousands of forged requests and measure microsecond response time differences to iteratively guess the signature one byte at a time. Using crypto.timingSafeEqual enforces uniform execution time regardless of where mismatches occur.

ts
// utils/webhook-security.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export interface VerifyWebhookOptions {
  rawBody: string;
  signatureHeader: string;
  secret: string;
  timestampHeader?: string | null;
  toleranceSeconds?: number;
}

/**
 * Validates an HMAC-SHA256 webhook signature with constant-time equality
 * and replay-window validation.
 */
export function verifyHmacSignature({
  rawBody,
  signatureHeader,
  secret,
  timestampHeader,
  toleranceSeconds = 300,
}: VerifyWebhookOptions): boolean {
  // 1. Replay attack validation
  if (timestampHeader) {
    const eventTime = Number.parseInt(timestampHeader, 10);
    const currentTime = Math.floor(Date.now() / 1000);

    if (Number.isNaN(eventTime) || Math.abs(currentTime - eventTime) > toleranceSeconds) {
      console.warn(`Webhook timestamp out of tolerance: ${eventTime}`);
      return false;
    }
  }

  // 2. Prepare payload to hash
  const payloadToSign = timestampHeader ? `${timestampHeader}.${rawBody}` : rawBody;

  // 3. Compute expected signature
  const expectedSignatureHex = createHmac('sha256', secret)
    .update(payloadToSign, 'utf8')
    .digest('hex');

  // Normalize signature from header (strip prefixes like sha256= or v1=)
  const cleanSignature = signatureHeader.replace(/^(sha256=|v1=)/, '').trim();

  // 4. Constant-time buffer comparison
  const expectedBuffer = Buffer.from(expectedSignatureHex, 'utf8');
  const providedBuffer = Buffer.from(cleanSignature, 'utf8');

  // Buffers must have identical length for timingSafeEqual
  if (expectedBuffer.length !== providedBuffer.length) {
    return false;
  }

  return timingSafeEqual(expectedBuffer, providedBuffer);
}

Zero-Downtime Secret Key Rotation

Signing secrets must be rotated periodically or immediately after employee turnover. If your verification code only checks a single secret, swapping secrets in production will reject all in-flight webhook deliveries.

Implement multi-secret verification:

ts
export function verifyWithRotation(
  payload: VerifyWebhookOptions,
  activeSecrets: string[]
): boolean {
  // Test candidate secrets until one matches
  for (const secret of activeSecrets) {
    const isValid = verifyHmacSignature({ ...payload, secret });
    if (isValid) {
      return true;
    }
  }
  return false;
}

During rotation:

  1. Generate new secret key SECRET_NEW.
  2. Deploy backend with activeSecrets = [SECRET_CURRENT, SECRET_NEW].
  3. Update secret in provider dashboard.
  4. Verify deliveries succeed using SECRET_NEW.
  5. Remove SECRET_CURRENT from backend configuration.

Verifying Signatures During Local Development with Probe

When integrating webhooks from complex third-party platforms, verifying raw HMAC signatures can be difficult without an inspection proxy.

With Probe, you can:

  • View raw headers and unformatted payload buffers side-by-side.
  • Inspect incoming X-Signature or Stripe-Signature values.
  • Verify whether whitespace changes or Unicode encodings occurred.
  • Copy verified payloads into your automated CI test pipeline.

Frequently Asked Questions

Probe Live Inspection
Ready to inspect incoming webhooks in real time?
Create a dedicated Probe endpoint to capture headers, verify raw byte payloads, and debug delivery failures before deploying to production.
Start debugging nowFree tier · Instant setup · TLS 1.3
Probe

Probe Team

Probe is a focused webhook debugging platform designed for engineering teams. We build telemetry and developer inspection tooling to eliminate integration guesswork.

All guides are regularly verified against Stripe, GitHub, Shopify, and RFC specifications.

More Webhook Guides
All articles
September 16, 20267 min read
A step-by-step developer checklist for diagnosing missing, timed-out, rejected, or duplicate webhook deliveries with reproducible test workflows.
#debugging#webhooks
Read guide
September 16, 20266 min read
A practical, battle-tested guide for capturing, verifying, and debugging Stripe webhooks locally without signature errors or payload corruption.
#Stripe#testing
Read guide

Developer Webhook Telemetry

Start debugging your webhooks with Probe

Never guess what an API sent again. Real-time payload inspection, raw headers, and replay tools.