Signature Verification
Webhook signature verification is crucial for security. This guide shows you how to verify that webhooks are actually coming from Zelta Pay and haven’t been tampered with.
Why Verify Signatures?
Section titled “Why Verify Signatures?”Signature verification ensures:
- Authenticity: The webhook is from Zelta Pay
- Integrity: The payload hasn’t been modified
- Security: Prevents replay attacks and spoofing
How It Works
Section titled “How It Works”Zelta Pay uses HMAC-SHA256 to sign webhook payloads:
- Create a signature using your webhook secret
- Include timestamp to prevent replay attacks
- Send signature in the
Zeltapay-Signatureheader - You verify the signature matches the payload
Signature Format
Section titled “Signature Format”The signature header contains:
Zeltapay-Signature: t=1640995200, v1=abc123def456...Where:
t: Unix timestamp when the webhook was sentv1: HMAC-SHA256 signature (64 hex characters)
Verification Algorithm
Section titled “Verification Algorithm”Step 1: Extract Components
Section titled “Step 1: Extract Components”const signatureHeader = req.headers['zeltapay-signature'];const timestamp = req.headers['zeltapay-timestamp'];
// Extract timestamp and signatureconst match = /t=(\d+),\s*v1=([a-f0-9]{64})/i.exec(signatureHeader);if (!match) { throw new Error('Invalid signature format');}
const [, t, v1] = match;Step 2: Verify Timestamp
Section titled “Step 2: Verify Timestamp”function verifyTimestamp(timestamp, tolerance = 300) { const now = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); const timeDiff = Math.abs(now - webhookTime);
return timeDiff <= tolerance;}
if (!verifyTimestamp(timestamp)) { throw new Error('Webhook timestamp too old');}Step 3: Create Expected Signature
Section titled “Step 3: Create Expected Signature”function createExpectedSignature(rawBody, timestamp, secret) { const input = `t=${timestamp}.${rawBody}`; const signature = crypto .createHmac('sha256', secret) .update(input) .digest('hex');
return signature;}
const expected = createExpectedSignature(rawBody, timestamp, secret);Step 4: Compare Signatures
Section titled “Step 4: Compare Signatures”function compareSignatures(expected, received) { const expectedBuffer = Buffer.from(expected, 'hex'); const receivedBuffer = Buffer.from(received, 'hex');
return expectedBuffer.length === receivedBuffer.length && crypto.timingSafeEqual(expectedBuffer, receivedBuffer);}
if (!compareSignatures(expected, v1)) { throw new Error('Invalid signature');}Complete Implementation
Section titled “Complete Implementation”Node.js Implementation
Section titled “Node.js Implementation”const crypto = require('crypto');
function verifyZeltapaySignature({ rawBody, timestamp, signatureHeader, secret,}) { // Extract timestamp and signature const match = /t=(\d+),\s*v1=([a-f0-9]{64})/i.exec(signatureHeader); if (!match) { return false; }
const [, t, v1] = match;
// Verify timestamp matches if (t !== timestamp) { return false; }
// Create expected signature const input = `t=${timestamp}.${rawBody}`; const expected = crypto .createHmac('sha256', secret) .update(input) .digest('hex');
// Compare signatures using timing-safe comparison const expectedBuffer = Buffer.from(expected, 'hex'); const receivedBuffer = Buffer.from(v1, 'hex');
return expectedBuffer.length === receivedBuffer.length && crypto.timingSafeEqual(expectedBuffer, receivedBuffer);}
// Usage in Expressapp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp']; const secret = process.env.WEBHOOK_SECRET;
if (!verifyZeltapaySignature({ rawBody: req.body.toString(), timestamp, signatureHeader: signature, secret })) { return res.status(401).json({ error: 'Invalid signature' }); }
// Signature is valid, process webhook const payload = JSON.parse(req.body); processWebhook(payload);
res.status(200).json({ received: true });});Cloudflare Workers Implementation
Section titled “Cloudflare Workers Implementation”export default { async fetch(request, env, ctx) { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); }
const url = new URL(request.url); if (url.pathname !== '/webhook') { return new Response('Not found', { status: 404 }); }
try { // Get headers const signature = request.headers.get('zeltapay-signature'); const timestamp = request.headers.get('zeltapay-timestamp');
// Get raw body const rawBody = await request.text();
// Verify signature if (!await verifyZeltapaySignature(rawBody, timestamp, signature, env.WEBHOOK_SECRET)) { return new Response(JSON.stringify({ error: 'Invalid signature' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); }
// Parse payload const payload = JSON.parse(rawBody);
// Process webhook await processWebhook(payload);
return new Response(JSON.stringify({ received: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) { console.error('Webhook processing error:', error); return new Response(JSON.stringify({ error: 'Processing failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }};
async function verifyZeltapaySignature(rawBody, timestamp, signatureHeader, secret) { // Extract timestamp and signature const match = /t=(\d+),\s*v1=([a-f0-9]{64})/i.exec(signatureHeader); if (!match) { return false; }
const [, t, v1] = match;
// Verify timestamp matches if (t !== timestamp) { return false; }
// Create expected signature using Web Crypto API const input = `t=${timestamp}.${rawBody}`; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input)); const expected = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');
// Compare signatures using timing-safe comparison return expected === v1;}
async function processWebhook(payload) { // Process the webhook event console.log('Processing webhook:', payload.type);}Hono with Cloudflare Workers Implementation
Section titled “Hono with Cloudflare Workers Implementation”import { Hono } from 'hono';import { cors } from 'hono/cors';
type Bindings = { WEBHOOK_SECRET: string;};
const app = new Hono<{ Bindings: Bindings }>();
// Enable CORSapp.use('*', cors());
app.post('/webhook', async (c) => { try { // Get headers const signature = c.req.header('zeltapay-signature'); const timestamp = c.req.header('zeltapay-timestamp');
if (!signature || !timestamp) { return c.json({ error: 'Missing required headers' }, 400); }
// Get raw body const rawBody = await c.req.text();
// Verify signature if (!await verifyZeltapaySignature(rawBody, timestamp, signature, c.env.WEBHOOK_SECRET)) { return c.json({ error: 'Invalid signature' }, 401); }
// Parse payload const payload = JSON.parse(rawBody);
// Process webhook await processWebhook(payload);
return c.json({ received: true });
} catch (error) { console.error('Webhook processing error:', error); return c.json({ error: 'Processing failed' }, 500); }});
async function verifyZeltapaySignature(rawBody: string, timestamp: string, signatureHeader: string, secret: string): Promise<boolean> { // Extract timestamp and signature const match = /t=(\d+),\s*v1=([a-f0-9]{64})/i.exec(signatureHeader); if (!match) { return false; }
const [, t, v1] = match;
// Verify timestamp matches if (t !== timestamp) { return false; }
// Create expected signature using Web Crypto API const input = `t=${timestamp}.${rawBody}`; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input)); const expected = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');
// Compare signatures using timing-safe comparison return expected === v1;}
async function processWebhook(payload: any) { // Process the webhook event console.log('Processing webhook:', payload.type);}
export default app;Python Implementation
Section titled “Python Implementation”import hmacimport hashlibimport reimport time
def verify_zeltapay_signature(raw_body, timestamp, signature_header, secret): """Verify Zelta Pay webhook signature"""
# Extract timestamp and signature match = re.search(r't=(\d+),\s*v1=([a-f0-9]{64})', signature_header, re.IGNORECASE) if not match: return False
t, v1 = match.groups()
# Verify timestamp matches if t != timestamp: return False
# Create expected signature input_string = f"t={timestamp}.{raw_body}" expected = hmac.new( secret.encode('utf-8'), input_string.encode('utf-8'), hashlib.sha256 ).hexdigest()
# Compare signatures using timing-safe comparison return hmac.compare_digest(expected, v1)
# Usage in Flask@app.route('/webhook', methods=['POST'])def webhook(): signature = request.headers.get('Zeltapay-Signature') timestamp = request.headers.get('Zeltapay-Timestamp') secret = os.getenv('WEBHOOK_SECRET')
# Get raw body raw_body = request.get_data(as_text=True)
if not verify_zeltapay_signature(raw_body, timestamp, signature, secret): return jsonify({'error': 'Invalid signature'}), 401
# Signature is valid, process webhook payload = request.get_json() process_webhook(payload)
return jsonify({'received': True}), 200PHP Implementation
Section titled “PHP Implementation”<?phpfunction verifyZeltapaySignature($rawBody, $timestamp, $signatureHeader, $secret) { // Extract timestamp and signature if (!preg_match('/t=(\d+),\s*v1=([a-f0-9]{64})/i', $signatureHeader, $matches)) { return false; }
$t = $matches[1]; $v1 = $matches[2];
// Verify timestamp matches if ($t !== $timestamp) { return false; }
// Create expected signature $input = "t={$timestamp}.{$rawBody}"; $expected = hash_hmac('sha256', $input, $secret);
// Compare signatures using timing-safe comparison return hash_equals($expected, $v1);}
// Usage$signature = $_SERVER['HTTP_ZELTAPAY_SIGNATURE'] ?? '';$timestamp = $_SERVER['HTTP_ZELTAPAY_TIMESTAMP'] ?? '';$secret = $_ENV['WEBHOOK_SECRET'] ?? '';
// Get raw body$rawBody = file_get_contents('php://input');
if (!verifyZeltapaySignature($rawBody, $timestamp, $signature, $secret)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit;}
// Signature is valid, process webhook$payload = json_decode($rawBody, true);processWebhook($payload);
http_response_code(200);echo json_encode(['received' => true]);?>Go Implementation
Section titled “Go Implementation”package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "net/http" "regexp" "strings")
func verifyZeltapaySignature(rawBody, timestamp, signatureHeader, secret string) bool { // Extract timestamp and signature re := regexp.MustCompile(`t=(\d+),\s*v1=([a-f0-9]{64})`) matches := re.FindStringSubmatch(signatureHeader) if len(matches) != 3 { return false }
t := matches[1] v1 := matches[2]
// Verify timestamp matches if t != timestamp { return false }
// Create expected signature input := fmt.Sprintf("t=%s.%s", timestamp, rawBody) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(input)) expected := hex.EncodeToString(mac.Sum(nil))
// Compare signatures using timing-safe comparison return hmac.Equal([]byte(expected), []byte(v1))}
func webhookHandler(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("Zeltapay-Signature") timestamp := r.Header.Get("Zeltapay-Timestamp") secret := os.Getenv("WEBHOOK_SECRET")
// Read raw body body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading body", http.StatusBadRequest) return }
rawBody := string(body)
if !verifyZeltapaySignature(rawBody, timestamp, signature, secret) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return }
// Signature is valid, process webhook var payload map[string]interface{} json.Unmarshal(body, &payload) processWebhook(payload)
w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"received": true})}Timestamp Validation
Section titled “Timestamp Validation”Why Validate Timestamps?
Section titled “Why Validate Timestamps?”Timestamps prevent replay attacks by ensuring webhooks are recent:
function validateTimestamp(timestamp) { const now = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); const timeDiff = Math.abs(now - webhookTime);
// Reject webhooks older than 5 minutes return timeDiff <= 300;}Complete Verification with Timestamp
Section titled “Complete Verification with Timestamp”Node.js Example
Section titled “Node.js Example”function verifyWebhook(req, secret) { const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp']; const rawBody = req.body.toString();
// Validate timestamp (5 minutes tolerance) const now = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); if (Math.abs(now - webhookTime) > 300) { throw new Error('Webhook timestamp too old'); }
// Verify signature if (!verifyZeltapaySignature({ rawBody, timestamp, signatureHeader: signature, secret })) { throw new Error('Invalid signature'); }
return true;}Cloudflare Workers Example
Section titled “Cloudflare Workers Example”async function verifyWebhook(request, secret) { const signature = request.headers.get('zeltapay-signature'); const timestamp = request.headers.get('zeltapay-timestamp'); const rawBody = await request.text();
// Validate timestamp (5 minutes tolerance) const now = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); if (Math.abs(now - webhookTime) > 300) { throw new Error('Webhook timestamp too old'); }
// Verify signature if (!await verifyZeltapaySignature(rawBody, timestamp, signature, secret)) { throw new Error('Invalid signature'); }
return true;}Hono Example
Section titled “Hono Example”async function verifyWebhook(c: Context, secret: string) { const signature = c.req.header('zeltapay-signature'); const timestamp = c.req.header('zeltapay-timestamp'); const rawBody = await c.req.text();
if (!signature || !timestamp) { throw new Error('Missing required headers'); }
// Validate timestamp (5 minutes tolerance) const now = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); if (Math.abs(now - webhookTime) > 300) { throw new Error('Webhook timestamp too old'); }
// Verify signature if (!await verifyZeltapaySignature(rawBody, timestamp, signature, secret)) { throw new Error('Invalid signature'); }
return true;}Python Example
Section titled “Python Example”import time
def verify_webhook(request, secret): signature = request.headers.get('Zeltapay-Signature') timestamp = request.headers.get('Zeltapay-Timestamp') raw_body = request.get_data(as_text=True)
# Validate timestamp (5 minutes tolerance) now = int(time.time()) webhook_time = int(timestamp) if abs(now - webhook_time) > 300: raise ValueError('Webhook timestamp too old')
# Verify signature if not verify_zeltapay_signature(raw_body, timestamp, signature, secret): raise ValueError('Invalid signature')
return TrueError Handling
Section titled “Error Handling”Common Verification Errors
Section titled “Common Verification Errors”function handleVerificationError(error) { switch (error.message) { case 'Invalid signature format': return { status: 400, message: 'Malformed signature header' }; case 'Invalid signature': return { status: 401, message: 'Signature verification failed' }; case 'Webhook timestamp too old': return { status: 400, message: 'Webhook timestamp expired' }; default: return { status: 500, message: 'Verification error' }; }}Robust Error Handling
Section titled “Robust Error Handling”Node.js Example
Section titled “Node.js Example”app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { try { const secret = process.env.WEBHOOK_SECRET; if (!secret) { throw new Error('Webhook secret not configured'); }
verifyWebhook(req, secret);
// Process webhook const payload = JSON.parse(req.body); processWebhook(payload);
res.status(200).json({ received: true });
} catch (error) { console.error('Webhook verification failed:', error);
const errorResponse = handleVerificationError(error); res.status(errorResponse.status).json({ error: errorResponse.message }); }});Cloudflare Workers Example
Section titled “Cloudflare Workers Example”export default { async fetch(request, env, ctx) { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); }
const url = new URL(request.url); if (url.pathname !== '/webhook') { return new Response('Not found', { status: 404 }); }
try { const secret = env.WEBHOOK_SECRET; if (!secret) { throw new Error('Webhook secret not configured'); }
await verifyWebhook(request, secret);
// Process webhook const payload = await request.json(); await processWebhook(payload);
return new Response(JSON.stringify({ received: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) { console.error('Webhook verification failed:', error);
const errorResponse = handleVerificationError(error); return new Response(JSON.stringify({ error: errorResponse.message }), { status: errorResponse.status, headers: { 'Content-Type': 'application/json' } }); } }};Hono Example
Section titled “Hono Example”app.post('/webhook', async (c) => { try { const secret = c.env.WEBHOOK_SECRET; if (!secret) { throw new Error('Webhook secret not configured'); }
await verifyWebhook(c, secret);
// Process webhook const payload = await c.req.json(); await processWebhook(payload);
return c.json({ received: true });
} catch (error) { console.error('Webhook verification failed:', error);
const errorResponse = handleVerificationError(error); return c.json({ error: errorResponse.message }, errorResponse.status); }});Python Example
Section titled “Python Example”@app.route('/webhook', methods=['POST'])def webhook(): try: secret = os.getenv('WEBHOOK_SECRET') if not secret: raise ValueError('Webhook secret not configured')
verify_webhook(request, secret)
# Process webhook payload = request.get_json() process_webhook(payload)
return jsonify({'received': True}), 200
except Exception as error: print(f'Webhook verification failed: {error}')
error_response = handle_verification_error(error) return jsonify({'error': error_response['message']}), error_response['status']Testing Signature Verification
Section titled “Testing Signature Verification”Test with Webhook Ping
Section titled “Test with Webhook Ping”Use the dashboard’s webhook ping feature to test your verification:
- Set up your webhook endpoint with signature verification
- Go to Settings → Webhooks in the dashboard
- Click “Test” next to your webhook
- Check your server logs for verification results
Manual Testing
Section titled “Manual Testing”Create a test script to verify your implementation:
Node.js Example
Section titled “Node.js Example”const crypto = require('crypto');
function testSignatureVerification() { const secret = 'test-secret'; const payload = JSON.stringify({ test: 'data' }); const timestamp = Math.floor(Date.now() / 1000).toString();
// Create signature const input = `t=${timestamp}.${payload}`; const signature = crypto .createHmac('sha256', secret) .update(input) .digest('hex');
const signatureHeader = `t=${timestamp}, v1=${signature}`;
// Test verification const isValid = verifyZeltapaySignature({ rawBody: payload, timestamp, signatureHeader, secret });
console.log('Signature verification test:', isValid ? 'PASSED' : 'FAILED');}
testSignatureVerification();Cloudflare Workers Example
Section titled “Cloudflare Workers Example”// Test endpoint for Cloudflare Workersexport default { async fetch(request, env, ctx) { if (request.method === 'POST' && new URL(request.url).pathname === '/test-signature') { const secret = 'test-secret'; const payload = JSON.stringify({ test: 'data' }); const timestamp = Math.floor(Date.now() / 1000).toString();
// Create signature using Web Crypto API const input = `t=${timestamp}.${payload}`; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input)); const signature = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');
const signatureHeader = `t=${timestamp}, v1=${signature}`;
// Test verification const isValid = await verifyZeltapaySignature(payload, timestamp, signatureHeader, secret);
return new Response(JSON.stringify({ test: 'Signature verification test', result: isValid ? 'PASSED' : 'FAILED', timestamp, signature: signatureHeader }), { headers: { 'Content-Type': 'application/json' } }); }
return new Response('Not found', { status: 404 }); }};Hono Example
Section titled “Hono Example”app.post('/test-signature', async (c) => { const secret = 'test-secret'; const payload = JSON.stringify({ test: 'data' }); const timestamp = Math.floor(Date.now() / 1000).toString();
// Create signature using Web Crypto API const input = `t=${timestamp}.${payload}`; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input)); const signature = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');
const signatureHeader = `t=${timestamp}, v1=${signature}`;
// Test verification const isValid = await verifyZeltapaySignature(payload, timestamp, signatureHeader, secret);
return c.json({ test: 'Signature verification test', result: isValid ? 'PASSED' : 'FAILED', timestamp, signature: signatureHeader });});Python Example
Section titled “Python Example”def test_signature_verification(): secret = 'test-secret' payload = json.dumps({'test': 'data'}) timestamp = str(int(time.time()))
# Create signature input_string = f"t={timestamp}.{payload}" signature = hmac.new( secret.encode('utf-8'), input_string.encode('utf-8'), hashlib.sha256 ).hexdigest()
signature_header = f"t={timestamp}, v1={signature}"
# Test verification is_valid = verify_zeltapay_signature(payload, timestamp, signature_header, secret)
print(f"Signature verification test: {'PASSED' if is_valid else 'FAILED'}")
test_signature_verification()Security Best Practices
Section titled “Security Best Practices”1. Use Timing-Safe Comparison
Section titled “1. Use Timing-Safe Comparison”Always use timing-safe comparison functions:
// ✅ Good - timing-safecrypto.timingSafeEqual(buffer1, buffer2)
// ❌ Bad - vulnerable to timing attackssignature1 === signature22. Validate Input Format
Section titled “2. Validate Input Format”Check signature header format before processing:
function validateSignatureFormat(signatureHeader) { const pattern = /^t=\d+,\s*v1=[a-f0-9]{64}$/i; return pattern.test(signatureHeader);}3. Handle Edge Cases
Section titled “3. Handle Edge Cases”function robustVerification(req, secret) { // Check required headers const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp'];
if (!signature || !timestamp) { throw new Error('Missing required headers'); }
// Validate format if (!validateSignatureFormat(signature)) { throw new Error('Invalid signature format'); }
// Validate timestamp if (!validateTimestamp(timestamp)) { throw new Error('Invalid timestamp'); }
// Verify signature if (!verifyZeltapaySignature({ rawBody: req.body.toString(), timestamp, signatureHeader: signature, secret })) { throw new Error('Invalid signature'); }
return true;}4. Log Security Events
Section titled “4. Log Security Events”Log failed verification attempts for monitoring:
function logSecurityEvent(event, details) { console.log(`[SECURITY] ${event}:`, { timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'], ...details });}
// Usageif (!verifySignature(req, secret)) { logSecurityEvent('INVALID_SIGNATURE', { signature: req.headers['zeltapay-signature'], timestamp: req.headers['zeltapay-timestamp'] }); throw new Error('Invalid signature');}Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”1. Signature Mismatch
- Check webhook secret is correct
- Ensure raw body is used (not parsed JSON)
- Verify timestamp format
- Check signature header format
2. Timestamp Validation Fails
- Check server time synchronization
- Adjust tolerance window if needed
- Consider timezone differences
3. Header Not Found
- Verify header names are case-sensitive
- Check middleware isn’t modifying headers
- Ensure raw body middleware is used
Debug Mode
Section titled “Debug Mode”Enable debug logging to troubleshoot:
// Node.js debug loggingfunction debugVerification(req, secret) { const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp']; const rawBody = req.body.toString();
console.log('Debug verification:', { signature, timestamp, rawBodyLength: rawBody.length, secretLength: secret.length });
// Continue with verification...}// Cloudflare Workers debug loggingexport default { async fetch(request, env, ctx) { if (request.method === 'POST' && new URL(request.url).pathname === '/webhook') { const signature = request.headers.get('zeltapay-signature'); const timestamp = request.headers.get('zeltapay-timestamp'); const rawBody = await request.text();
console.log('Debug verification:', { signature, timestamp, rawBodyLength: rawBody.length, secretLength: env.WEBHOOK_SECRET?.length || 0, userAgent: request.headers.get('user-agent'), cfRay: request.headers.get('cf-ray') });
// Continue with verification... }
return new Response('Not found', { status: 404 }); }};// Hono debug loggingasync function debugVerification(c: Context, secret: string) { const signature = c.req.header('zeltapay-signature'); const timestamp = c.req.header('zeltapay-timestamp'); const rawBody = await c.req.text();
console.log('Debug verification:', { signature, timestamp, rawBodyLength: rawBody.length, secretLength: secret.length, userAgent: c.req.header('user-agent'), cfRay: c.req.header('cf-ray') });
// Continue with verification...}Next Steps
Section titled “Next Steps”Now that you understand signature verification, explore these resources:
- Webhooks Guide - Complete webhook setup guide
- Webhook Delivery Guide - Delivery mechanisms and retry logic
- Webhook Idempotency Guide - Prevent duplicate processing
- Webhook Events API - Event types and payloads
- Use Cases - See signature verification in action