Webhook Authentication

Webhook Signature Verification Guide

This guide explains how to verify webhook signatures sent by our system using digital signatures to ensure message authenticity and integrity.

Overview

Each webhook request is signed with our private Ed25519 key. The request includes specific headers that are used to construct a verification message, which you can validate using the corresponding public key.

Webhook Headers

Webhook requests include the following headers:

HeaderDescription
X-Webhook-SignatureBase64-encoded Ed25519 signature
X-Webhook-Content-DigestSHA-512 digest of the request body
X-Webhook-Event-IdUnique identifier for the event
X-Webhook-Event-TimestampISO 8601 timestamp when the event occurred
X-Webhook-Request-IdUnique identifier for the request
X-Webhook-Request-TimestampISO 8601 timestamp when the request was sent
X-Webhook-Key-VersionVersion of the signing key used

Signature Verification Process

1. Construct the Verification Message

Concatenate the following header values using a | (pipe) character:

Content-Digest | Event-Id | Event-Timestamp | Request-Id | Request-Timestamp | Key-Version

2. Use the Public Key

Verify the signature using the appropriate version of the public key provided in the X-Webhook-Key-Version header.

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEANSasj3xgjFkA1cp/3WCm1rA17CE1LXu77TvgB05QK8U=
-----END PUBLIC KEY-----

3. Verify the Signature

Steps:

  • Extract the X-Webhook-Signature header and decode it from Base64.
  • Construct the verification message as described above.
  • Verify the signature using the Ed25519 algorithm and the correct public key.

Example Implementations

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives import serialization
import base64

# Public key (replace with the actual key provided)
public_key_pem = b"""
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEANSasj3xgjFkA1cp/3WCm1rA17CE1LXu77TvgB05QK8U=
-----END PUBLIC KEY-----
"""

def verify_webhook_signature(headers, signature_b64):
    """
    Verify webhook signature
    
    Args:
        headers: Dictionary containing webhook headers
        signature_b64: Base64-encoded signature from X-Webhook-Signature header
    
    Returns:
        bool: True if signature is valid, False otherwise
    """
    
    # Construct verification message
    message_parts = [
        headers.get('X-Webhook-Content-Digest', ''),
        headers.get('X-Webhook-Event-Id', ''),
        headers.get('X-Webhook-Event-Timestamp', ''),
        headers.get('X-Webhook-Request-Id', ''),
        headers.get('X-Webhook-Request-Timestamp', ''),
        headers.get('X-Webhook-Key-Version', '')
    ]
    
    message = '|'.join(message_parts).encode('utf-8')
    
    # Decode signature
    try:
        signature = base64.b64decode(signature_b64)
    except Exception:
        return False
    
    # Load public key
    try:
        public_key = serialization.load_pem_public_key(public_key_pem)
    except Exception:
        return False
    
    # Verify signature
    try:
        public_key.verify(signature, message)
        return True
    except Exception:
        return False

# Example usage
headers = {
    'X-Webhook-Signature': 'mfOXYn/rSEor0YoJ6fu1l9gwtLywYUtSVkgq6gXJLl6pdcN0ocPg65j5fmI9C+Ltefrb12jYheTddszOWAdYBQ==',
    'X-Webhook-Content-Digest': 'nnveBmTJUjrKljwEfvEv+Ku9FFMwBHe+fZxq9G6gbsKkiqbotmT2Uj7TkqAqowuB0DJKPwleZYrC0pVuS9609w==',
    'X-Webhook-Event-Id': 'c403c4fc-b1c5-4a2f-af57-3db63834cbef',
    'X-Webhook-Event-Timestamp': '2025-07-10T14:56:37.725866',
    'X-Webhook-Request-Id': '31dd03e6-9519-4290-bfc6-9ebf87bdeded',
    'X-Webhook-Request-Timestamp': '2025-07-10T14:56:39.908911748',
    'X-Webhook-Key-Version': '1'
}

signature = headers['X-Webhook-Signature']
is_valid = verify_webhook_signature(headers, signature)
print(f"Signature is {'VALID' if is_valid else 'INVALID'}")
const crypto = require('crypto');

// Public key in PEM format
const publicKeyPem = `
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEANSasj3xgjFkA1cp/3WCm1rA17CE1LXu77TvgB05QK8U=
-----END PUBLIC KEY-----
`;

function verifyWebhookSignature(headers, signatureB64) {
    try {
        // Construct verification message
        const messageParts = [
            headers['x-webhook-content-digest'] || '',
            headers['x-webhook-event-id'] || '',
            headers['x-webhook-event-timestamp'] || '',
            headers['x-webhook-request-id'] || '',
            headers['x-webhook-request-timestamp'] || '',
            headers['x-webhook-key-version'] || ''
        ];
        
        const message = messageParts.join('|');
        
        // Decode signature
        const signature = Buffer.from(signatureB64, 'base64');
        
        // Create public key object
        const publicKey = crypto.createPublicKey(publicKeyPem);
        
        // Verify signature
        return crypto.verify(null, Buffer.from(message, 'utf-8'), publicKey, signature);
    } catch (error) {
        console.error('Signature verification error:', error);
        return false;
    }
}

// Example usage
const headers = {
    'x-webhook-signature': 'mfOXYn/rSEor0YoJ6fu1l9gwtLywYUtSVkgq6gXJLl6pdcN0ocPg65j5fmI9C+Ltefrb12jYheTddszOWAdYBQ==',
    'x-webhook-content-digest': 'nnveBmTJUjrKljwEfvEv+Ku9FFMwBHe+fZxq9G6gbsKkiqbotmT2Uj7TkqAqowuB0DJKPwleZYrC0pVuS9609w==',
    'x-webhook-event-id': 'c403c4fc-b1c5-4a2f-af57-3db63834cbef',
    'x-webhook-event-timestamp': '2025-07-10T14:56:37.725866',
    'x-webhook-request-id': '31dd03e6-9519-4290-bfc6-9ebf87bdeded',
    'x-webhook-request-timestamp': '2025-07-10T14:56:39.908911748',
    'x-webhook-key-version': '1'
};

const isValid = verifyWebhookSignature(headers, headers['x-webhook-signature']);
console.log(`Signature is ${isValid ? 'VALID' : 'INVALID'}`);
package main

import (
    "crypto/ed25519"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "fmt"
    "strings"
)

const publicKeyPEM = `
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEANSasj3xgjFkA1cp/3WCm1rA17CE1LXu77TvgB05QK8U=
-----END PUBLIC KEY-----
`

func verifyWebhookSignature(headers map[string]string, signatureB64 string) bool {
    // Construct verification message
    messageParts := []string{
        headers["X-Webhook-Content-Digest"],
        headers["X-Webhook-Event-Id"],
        headers["X-Webhook-Event-Timestamp"],
        headers["X-Webhook-Request-Id"],
        headers["X-Webhook-Request-Timestamp"],
        headers["X-Webhook-Key-Version"],
    }
    
    message := strings.Join(messageParts, "|")
    
    // Decode signature
    signature, err := base64.StdEncoding.DecodeString(signatureB64)
    if err != nil {
        return false
    }
    
    // Parse public key
    block, _ := pem.Decode([]byte(publicKeyPEM))
    if block == nil {
        return false
    }
    
    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return false
    }
    
    publicKey, ok := pub.(ed25519.PublicKey)
    if !ok {
        return false
    }
    
    // Verify signature
    return ed25519.Verify(publicKey, []byte(message), signature)
}

func main() {
    headers := map[string]string{
        "X-Webhook-Signature": "mfOXYn/rSEor0YoJ6fu1l9gwtLywYUtSVkgq6gXJLl6pdcN0ocPg65j5fmI9C+Ltefrb12jYheTddszOWAdYBQ==",
        "X-Webhook-Content-Digest": "nnveBmTJUjrKljwEfvEv+Ku9FFMwBHe+fZxq9G6gbsKkiqbotmT2Uj7TkqAqowuB0DJKPwleZYrC0pVuS9609w==",
        "X-Webhook-Event-Id": "c403c4fc-b1c5-4a2f-af57-3db63834cbef",
        "X-Webhook-Event-Timestamp": "2025-07-10T14:56:37.725866",
        "X-Webhook-Request-Id": "31dd03e6-9519-4290-bfc6-9ebf87bdeded",
        "X-Webhook-Request-Timestamp": "2025-07-10T14:56:39.908911748",
        "X-Webhook-Key-Version": "1",
    }
    
    isValid := verifyWebhookSignature(headers, headers["X-Webhook-Signature"])
    if isValid {
        fmt.Println("Signature is VALID ✔️")
    } else {
        fmt.Println("Signature is INVALID ❌")
    }
}

Testing Your Implementation

Use the example headers and signature values in the provided code sample to test your function. If your implementation returns true, the signature verification is working correctly.

Dependencies

LanguageRequirement
Pythoncryptography (pip install cryptography)
Node.jsBuilt-in crypto module
GoStandard library

Common Issues

If verification fails, double-check:

  1. Headers are concatenated exactly in the expected order with | as the separator.
  2. Ensure proper base64 decoding of the signature
  3. Using the correct public key version
  4. Your Ed25519 implementation is working correctly