Webhook Verification
Verify webhook signatures to ensure requests are authentic.
Why Verify?
Webhook signatures prove that:
- The request came from ZKP2P Pay (not an attacker)
- The payload hasn't been tampered with
- The webhook isn't a replay of an old request
Signature Format
Each webhook includes these headers:
| Header | Description |
|---|---|
X-Webhook-Id | Unique event ID |
X-Webhook-Timestamp | Unix timestamp (seconds) |
X-Webhook-Signature | HMAC-SHA256 signature (hex) |
Verification Algorithm
The signature is computed as:
HMAC-SHA256(secret, timestamp + "." + payload)
Where:
secretis your webhook secret (from webhook creation)timestampis fromX-Webhook-Timestampheaderpayloadis the raw request body
Implementation Examples
Node.js / Express
import express from 'express';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
function verifyWebhookSignature(
payload: string,
signature: string,
timestamp: string
): boolean {
// Check timestamp is recent (within 5 minutes)
const now = Math.floor(Date.now() / 1000);
const webhookTimestamp = parseInt(timestamp, 10);
if (Math.abs(now - webhookTimestamp) > 300) {
console.error('Webhook timestamp too old');
return false;
}
// Compute expected signature
const signatureBase = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signatureBase)
.digest('hex');
// Use timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch {
return false;
}
}
const app = express();
app.post(
'/webhooks/zkp2p',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process event...
res.status(200).send('OK');
}
);
Python / Flask
import hmac
import hashlib
import time
from flask import Flask, request, abort
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']
def verify_webhook_signature(payload: bytes, signature: str, timestamp: str) -> bool:
# Check timestamp is recent
now = int(time.time())
webhook_timestamp = int(timestamp)
if abs(now - webhook_timestamp) > 300:
return False
# Compute expected signature
signature_base = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
signature_base.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Timing-safe comparison
return hmac.compare_digest(signature, expected_signature)
app = Flask(__name__)
@app.route('/webhooks/zkp2p', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
payload = request.data
if not verify_webhook_signature(payload, signature, timestamp):
abort(401)
event = request.get_json()
# Process event...
return 'OK', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"time"
)
var webhookSecret = []byte(os.Getenv("WEBHOOK_SECRET"))
func verifyWebhookSignature(payload []byte, signature, timestamp string) bool {
// Check timestamp
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
if abs(now-ts) > 300 {
return false
}
// Compute expected signature
signatureBase := timestamp + "." + string(payload)
mac := hmac.New(sha256.New, webhookSecret)
mac.Write([]byte(signatureBase))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Timing-safe comparison
return hmac.Equal(
[]byte(signature),
[]byte(expectedSignature),
)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
payload, _ := io.ReadAll(r.Body)
if !verifyWebhookSignature(payload, signature, timestamp) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process event...
w.WriteHeader(http.StatusOK)
}
Security Best Practices
1. Always Verify Signatures
Never skip signature verification, even in development.
2. Check Timestamp
Reject webhooks older than 5 minutes to prevent replay attacks:
const MAX_AGE_SECONDS = 300; // 5 minutes
const webhookAge = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (webhookAge > MAX_AGE_SECONDS) {
throw new Error('Webhook too old');
}
3. Use Timing-Safe Comparison
Always use timing-safe comparison to prevent timing attacks:
// Good: timing-safe
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
// Bad: vulnerable to timing attacks
a === b;
4. Use Raw Body
Parse the body after verification to ensure you're verifying the exact bytes received:
// Express
app.post('/webhook', express.raw({ type: 'application/json' }), handler);
// Then parse after verification
const event = JSON.parse(rawBody.toString());
5. Keep Secret Secure
- Store the webhook secret in environment variables
- Never log the secret
- Rotate secrets if compromised