Dashboard
WebhooksSecure Webhook Signature Verification | BotSubscription

Signature Verification

When you receive webhook notifications from BotSubscription, you must verify their authenticity and integrity before processing any payload or database side effects. This ensures that the requests originate from our secure servers and have not been tampered with in transit.

When integrating webhooks to synchronize events with your own custom backend or database, verifying signatures is a critical requirement for protecting your system against spoofing and replay attacks.


The Signing Secret

When you create or inspect a webhook endpoint in your dashboard, you are issued a 64-character hexadecimal Signing Secret (e.g., 64_character_hex_string_here...).

Keep this secret secure. Do not commit it to version control or expose it on client-side code. Treat it like an API key.


Signature Header Format

Every HTTP POST request sent to your receiver includes a custom security header:

X-Webhook-Signature: v1=9b724ac8f...,t=1779836400

The header contains a comma-separated list of key=value pairs:

  • v1: The signature computed using HMAC-SHA256 (64 hex characters).
  • t: The Unix epoch timestamp (in seconds) representing when the signature was generated.

Signature Computation

To verify a request, compute the expected HMAC-SHA256 signature locally on your backend and compare it against the value provided in the header.

1. Build the Payload String

Concatenate the timestamp t, a literal period ., and the exact raw HTTP request body bytes:

<timestamp_value>.<raw_http_request_body>
Warning

Use raw bytes: You must read the request body in its raw, unparsed form. Standard web application frameworks often parse JSON into objects automatically, which changes the whitespace, key order, or byte representation. Using parsed or re-serialized JSON will cause signature validation to fail.

2. Compute the HMAC

Calculate a Keyed-Hash Message Authentication Code (HMAC) using the SHA-256 hash function, using your endpoint's Signing Secret as the key.


Step-by-Step Verification Flow

  1. Extract the Header: Parse the X-Webhook-Signature header to extract the values of t and v1.
  2. Validate Time Drift: Check the timestamp t against your server's current time. Reject the request if the difference exceeds 5 minutes (300 seconds) to prevent replay attacks.
  3. Compute the Local Signature: Using the secret and the raw payload string <t>.<raw_body>, compute the HMAC-SHA256 signature.
  4. Perform a Safe Comparison: Compare your computed signature with the v1 value using a constant-time comparison helper. This prevents timing analysis attacks.

Interactive Webhook Validator

Use this purely client-side sandbox to verify your signature computation logic or to generate valid signatures for manual testing. None of your input data or secrets are sent to any server.

Your webhook endpoint's secret key. Stays entirely local in your browser.
Paste the full X-Webhook-Signature header value from the received request.
The exact raw HTTP request body string. Make sure there are no modified whitespaces.
Verification Result
Fill out the signing secret, signature header, and JSON payload above to run verification.

Implementation Examples

Select your programming language to view a complete, copy-pasteable example of HMAC-SHA256 signature verification following the flow detailed above.

import crypto from 'crypto';
 
/**
 * Verify Webhook Signature
 * @param {string} rawBody - Raw unparsed HTTP request body string
 * @param {string} signatureHeader - Value of X-Webhook-Signature header
 * @param {string} secret - Outbound signing secret from your project dashboard
 * @returns {boolean} True if signature matches and request is authentic
 */
function verifyWebhook(rawBody, signatureHeader, secret) {
  // 1. Parse signature header
  const parts = signatureHeader.split(',').reduce((acc, part) => {
    const [key, val] = part.split('=');
    if (key && val) acc[key] = val;
    return acc;
  }, {});
 
  const timestamp = parts.t;
  const signature = parts.v1;
 
  if (!timestamp || !signature) {
    throw new Error('Missing timestamp or signature');
  }
 
  // 2. Prevent replay attacks (reject if signature is > 5 minutes old)
  const allowedDrift = 5 * 60; // 5 minutes in seconds
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > allowedDrift) {
    throw new Error('Timestamp drift too high');
  }
 
  // 3. Construct string to sign
  const stringToSign = `${timestamp}.${rawBody}`;
 
  // 4. Compute the expected HMAC-SHA256 signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(stringToSign, 'utf8')
    .digest('hex');
 
  // 5. Compare using a constant-time check
  const isMatch = crypto.timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expectedSignature, 'utf8')
  );
 
  return isMatch;
}

Next Steps

Now that your receiver is secure, learn how to handle delivery scenarios:

Last updated: