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:
Header | Description |
---|---|
X-Webhook-Signature | Base64-encoded Ed25519 signature |
X-Webhook-Content-Digest | SHA-512 digest of the request body |
X-Webhook-Event-Id | Unique identifier for the event |
X-Webhook-Event-Timestamp | ISO 8601 timestamp when the event occurred |
X-Webhook-Request-Id | Unique identifier for the request |
X-Webhook-Request-Timestamp | ISO 8601 timestamp when the request was sent |
X-Webhook-Key-Version | Version 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
Language | Requirement |
---|---|
Python | cryptography (pip install cryptography ) |
Node.js | Built-in crypto module |
Go | Standard library |
Common Issues
If verification fails, double-check:
- Headers are concatenated exactly in the expected order with | as the separator.
- Ensure proper base64 decoding of the signature
- Using the correct public key version
- Your Ed25519 implementation is working correctly
Updated 1 day ago