Webhooks
Webhook Authentication and Webhook Retry Mechanism
Webhook Signature Verification Guide (Authentication)
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 | Base64-encoded SHA-512 digest of the raw request body bytes |
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
⚠️ Security Warning
Do not rely on the Content-Digest header value as the source of truth. Always recompute the digest yourself from the raw request body bytes. The header should only be used for comparison.
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----------BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAtvWVBXNwIC6PkLPUejhsTxC1MFEmgyP4h8V0mRhyyG8=
-----END PUBLIC KEY-----3. Verify the Signature
Steps:
- Extract the
X-Webhook-Signatureheader 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
Webhook Retry Mechanism
Overview
The Automatic Webhook Retry System ensures that webhook notifications are delivered reliably even when the receiving endpoint experiences temporary failures.
When a webhook delivery fails (for example, due to timeout or non-2xx response), the system automatically retries sending the webhook up to 5 times:
| Retry Attempt | Delay Before Retry |
|---|---|
| 1st retry | 5 minutes |
| 2nd retry | 15 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 1 hour |
After the 5th attempt fails, no further automatic retries will be performed.
If you need to re-trigger webhook delivery manually, the platform provides a Webhook Retry API, which allows you to request a retry for a specific webhook ID — provided that:
- The webhook has a failed status
- The webhook is not in progress
- The webhook hasn’t already been successfully processed
If the webhook is already successful or in progress, the retry endpoint will respond with an error. You can also retrieve all webhook records using the Webhook List API, supporting pagination and metadata filtering.
Webhook Object Field Definitions:
| Field | Type | Description |
|---|---|---|
id | UUID | Webhook id |
eventId | UUID | Unique Id of the event that triggered the webhook. Every webhook has its own unique Id, which can be trusted by the IF system. |
status | enum | Webhook status (processing, successful, failed) |
numberOfAttempts | integer | Number of retries performed so far |
requestPayload | object map | Webhook request payload |
eventDateTime | datetime | Event Creation datetime |
lastAttemptDateTime | datetime | Last webhook retry datetime |
responseStatusCode | integer | Timestamp of last retry attempt |
responsePayload | string | Webhook response payload |
responseHeaders | object map | Webhook response headers |
lastAttemptErrorMessage | string | Last Attempt error message if any error occurred. |
Webhook Retry Conditions:
| Status | Allowed to Retry? | Reason |
|---|---|---|
failed | ✔ Yes | Webhook delivery failed |
proccessing | ❌ No | Delivery is already being processed |
successful | ❌ No | Already delivered successfully |
List Webhooks API
A new paginated endpoint allows clients to retrieve webhook history, including delivery attempts, retry counts, timestamps, and payloads. List Webhook API
Webhook Retry API (Manual Retry)
A dedicated API endpoint now allows clients to manually retry a webhook exactly once, using the webhook’s unique identifier. Retry Webhook API
Updated 4 days ago