How to Verify Webhook Authenticity

Implement HMAC-SHA256 signature verification to ensure webhook requests are authentic and haven't been tampered with.

How to Verify Webhook Authenticity

Problem

You need to verify that incoming webhook requests are actually from PandaDoc and haven't been tampered with, preventing spoofed requests from malicious actors.

Prerequisites

  • Active webhook subscription
  • Access to your shared key from the Developer Dashboard(Dev Center)
  • Basic understanding of HMAC-SHA256 cryptographic signatures

Solution

Step 1: Retrieve Your Shared Key

  1. Navigate to the Developer Dashboard
  2. Locate your webhook subscription
  3. Copy the shared key (treat this like a password)
🚧

Protect Your Shared Key

Treat your shared key like a password to prevent others from generating a webhook with a payload that could pass a signature verification test.

Step 2: Configure Your Webhook URL

When creating your webhook subscription, configure your URL to accept the signature parameter:

https://yourdomain.com/webhook-handler/?signature={signature}

Step 3: Implement Signature Verification

Choose your preferred programming language:

import hmac
import hashlib

def verify_webhook_signature(shared_key, request_body, received_signature):
    # Generate expected signature
    expected_signature = hmac.new(
        str(shared_key).encode('utf-8'),
        str(request_body).encode('utf-8'),
        digestmod=hashlib.sha256
    ).hexdigest()

    # Use timing-safe comparison
    return hmac.compare_digest(expected_signature, received_signature)
<?php
function verifyWebhookSignature($sharedKey, $requestBody, $receivedSignature) {
    // Generate expected signature
    $expectedSignature = hash_hmac('sha256', $requestBody, $sharedKey);

    // Use timing-safe comparison
    return hash_equals($expectedSignature, $receivedSignature);
}
?>
using System;
using System.Security.Cryptography;
using System.Text;

public static bool VerifyHMACSHA256(string sharedKey, string requestBody, string receivedSignature) {
    byte[] key = Encoding.UTF8.GetBytes(sharedKey);
    using (HMACSHA256 hmac256 = new HMACSHA256(key))
    {
        var hashedSource = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestBody));

        if (hashedSource == null || hashedSource.Length == 0)
        {
            return false;
        }

        var expectedSignature = new StringBuilder();
        for (int i = 0; i < hashedSource.Length; i++)
        {
            expectedSignature.AppendFormat("{0:x2}", hashedSource[i]);
        }

        // Use timing-safe comparison
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expectedSignature.ToString()),
            Encoding.UTF8.GetBytes(receivedSignature)
        );
    }
}
const crypto = require("crypto");

function verifyWebhookSignature(sharedKey, requestBody, receivedSignature) {
  // Generate expected signature
  const hmac = crypto.createHmac("sha256", sharedKey);
  hmac.update(requestBody);
  const expectedSignature = hmac.digest("hex");

  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, "hex"),
    Buffer.from(receivedSignature, "hex")
  );
}

Step 4: Handle Verification Failures

When signature verification fails:

  1. Log the event for security monitoring
  2. Return HTTP 403 (Forbidden) status
  3. Do not process the webhook payload
  4. Alert your security team if multiple failures occur

Step 5: Test Your Implementation

  1. Create a test webhook subscription
  2. Trigger a document event
  3. Verify your endpoint correctly validates the signature
  4. Test with an invalid signature to confirm rejection

Verification

To confirm your implementation works:

  1. Valid signature test: Process a real webhook and verify it passes
  2. Invalid signature test: Modify the received signature and confirm it's rejected
  3. Timing attack protection: Use timing-safe comparison functions
  4. Error handling: Confirm proper HTTP status codes are returned

Troubleshooting

Common Issues

Signature verification always fails:

  • Ensure you're using the raw HTTP body (not parsed JSON)
  • Check that your shared key is correct
  • Verify the signature is extracted from the correct URL parameter

Security warnings:

  • Use hmac.compare_digest() in Python or crypto.timingSafeEqual() in Node.js
  • Avoid string comparison operators that are vulnerable to timing attacks

Performance concerns:

  • Signature verification is fast (microseconds)
  • Cache the shared key rather than fetching it for each request
  • Consider implementing rate limiting

Security Best Practices

  1. Store shared keys securely - Use environment variables or secure vaults
  2. Use timing-safe comparison - Prevent timing attack vulnerabilities
  3. Implement logging - Track verification failures for security monitoring
  4. Regular key rotation - Update shared keys periodically
  5. IP allowlisting - Combine with signature verification for defense in depth

Related