Webhooks
Webhooks allow you to receive real-time notifications when payments are completed. This guide covers how to set up, configure, and handle webhook events from Zelta Pay.
What are Webhooks?
Section titled “What are Webhooks?”Webhooks are HTTP callbacks that notify your application when specific events occur. Instead of polling the API for updates, Zelta Pay sends a POST request to your endpoint when a payment is completed.
Benefits
Section titled “Benefits”- Real-time notifications - Get instant updates when payments complete
- Efficient - No need to poll the API repeatedly
- Reliable - Built-in retry logic ensures delivery
- Secure - HMAC signature verification prevents spoofing
Quick Start
Section titled “Quick Start”1. Create Your Webhook Endpoint
Section titled “1. Create Your Webhook Endpoint”Your webhook endpoint must:
- Accept POST requests
- Return a 2xx status code for successful processing
- Be accessible via HTTPS (required for security)
- Handle the webhook payload
2. Configure Webhook in Dashboard
Section titled “2. Configure Webhook in Dashboard”- Log in to your Zelta Pay Dashboard
- Navigate to Webhooks
- Click Add Webhook
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive
- Test your webhook endpoint
Implementation Examples
Section titled “Implementation Examples”Node.js Example (Express)
Section titled “Node.js Example (Express)”const express = require('express');const app = express();
// Middleware to get raw body for signature verificationapp.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => { const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp'];
const payload = JSON.parse(req.body);
if (payload.type === 'payment.success') { console.log('Payment completed:', payload.transaction.amount); // Update your database, send confirmation email, etc. }
res.status(200).json({ received: true });});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 { // Get headers const signature = request.headers.get('zeltapay-signature'); const timestamp = request.headers.get('zeltapay-timestamp');
// Get raw body const rawBody = await request.text();
// Parse payload const payload = JSON.parse(rawBody);
if (payload.type === 'payment.success') { console.log('Payment completed:', payload.transaction.amount); // Update your database, send confirmation email, etc. }
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' } }); } }};Hono with Cloudflare Workers Example
Section titled “Hono with Cloudflare Workers Example”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();
// Parse payload const payload = JSON.parse(rawBody);
if (payload.type === 'payment.success') { console.log('Payment completed:', payload.transaction.amount); // Update your database, send confirmation email, etc. }
return c.json({ received: true });
} catch (error) { console.error('Webhook processing error:', error); return c.json({ error: 'Processing failed' }, 500); }});export default app;Webhook Events
Section titled “Webhook Events”Zelta Pay sends different types of webhook events to notify your application about important changes. The main events you’ll receive are:
Event Types
Section titled “Event Types”payment.success- Sent when a customer successfully completes a paymentwebhook.ping- Sent when you test your webhook endpoint
Event Structure
Section titled “Event Structure”All webhook events follow a consistent structure with common fields and event-specific data. For complete details about event types, payloads, and field descriptions, see the Webhook Events API Reference.
Processing Events
Section titled “Processing Events”function handleWebhookEvent(payload) { switch (payload.type) { case 'payment.success': handlePaymentSuccess(payload); break; case 'webhook.ping': console.log('Webhook ping received:', payload.message); break; default: console.log('Unknown event type:', payload.type); }}
function handlePaymentSuccess(payload) { const { transaction, customer, metadata } = payload;
console.log('Payment completed:', { amount: transaction.amount, currency: transaction.currency, customer: customer.customerName, orderId: metadata.orderId });
// Update your database, send confirmation email, etc.}Webhook Headers
Section titled “Webhook Headers”Every webhook request includes standardized headers for identification, security, and delivery tracking. The most important headers for processing webhooks are:
| Header | Description | Example |
|---|---|---|
Zeltapay-Event-Id | Unique event identifier | evt_1234567890abcdef |
Zeltapay-Event-Type | Event type | payment.success |
Zeltapay-Timestamp | Unix timestamp | 1640995200 |
Zeltapay-Signature | HMAC signature | t=1640995200, v1=abc123... |
Zeltapay-Delivery-Attempt | Attempt number | 1 |
For a complete list of headers and their descriptions, see the Webhook Events API Reference.
Delivery and Retry Logic
Section titled “Delivery and Retry Logic”Zelta Pay uses a robust delivery system to ensure webhook events reach your endpoint. The system includes:
Key Features
Section titled “Key Features”- Immediate delivery - Events are sent within 1 second
- Automatic retries - Up to 6 attempts over 24 hours
- Exponential backoff - Smart retry timing with jitter
- Dead Letter Queue - Failed deliveries are stored for manual review
Response Handling
Section titled “Response Handling”Your webhook endpoint should return appropriate HTTP status codes:
- 2xx (Success) - Event acknowledged and removed from queue
- 4xx (Client Error) - Event moved to Dead Letter Queue
- 5xx (Server Error) - Event retried with exponential backoff
Best Practices
Section titled “Best Practices”// Return appropriate status codesapp.post('/webhook', (req, res) => { try { // Process webhook processWebhookEvent(req.body);
// Success - event will be acknowledged res.status(200).json({ received: true });
} catch (error) { if (error.message.includes('validation')) { // Client error - event goes to DLQ return res.status(400).json({ error: 'Bad request' }); }
// Server error - event will be retried return res.status(500).json({ error: 'Internal server error' }); }});For complete details about retry schedules, Dead Letter Queue management, and monitoring, see the Webhook Delivery Guide.
Idempotency
Section titled “Idempotency”Idempotency ensures that processing the same webhook event multiple times produces the same result. This is crucial for webhook reliability.
Why Idempotency Matters
Section titled “Why Idempotency Matters”- Network issues - Webhook delivery fails, gets retried
- Server restarts - Webhook processed before restart, retried after
- Race conditions - Multiple webhook deliveries arrive simultaneously
- Manual retries - Events manually retried from dashboard
Basic Implementation
Section titled “Basic Implementation”const processedEvents = new Set();
function handleWebhookEvent(payload) { const eventId = payload.eventId;
if (processedEvents.has(eventId)) { console.log(`Duplicate event ignored: ${eventId}`); return; }
processedEvents.add(eventId);
// Process event... processPaymentSuccess(payload);}Database-Level Idempotency
Section titled “Database-Level Idempotency”async function processPaymentSuccess(payload) { const eventId = payload.eventId; const orderId = payload.metadata.orderId;
try { await db.transaction(async (trx) => { // Check if already processed const existing = await trx('webhook_idempotency') .where('event_id', eventId) .first();
if (existing) { console.log(`Event ${eventId} already processed`); return; }
// Mark as processing await trx('webhook_idempotency').insert({ event_id: eventId, status: 'processing' });
// Process payment await trx('orders') .where('id', orderId) .update({ status: 'paid' }); }); } catch (error) { if (error.code === 'ER_DUP_ENTRY') { console.log(`Event ${eventId} already processed`); return; } throw error; }}For complete implementation strategies, database schemas, and best practices, see the Webhook Idempotency Guide.
Best Practices
Section titled “Best Practices”1. Respond Quickly
Section titled “1. Respond Quickly”Keep webhook processing fast to avoid timeouts:
// Node.js - Respond immediately, process asynchronouslyapp.post('/webhook', (req, res) => { res.status(200).json({ received: true });
setImmediate(async () => { try { const payload = JSON.parse(req.body); await processWebhookEvent(payload); } catch (error) { console.error('Async webhook processing error:', error); } });});// Cloudflare Workers - Use background tasksexport default { async fetch(request, env, ctx) { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); }
try { const payload = await request.json();
// Respond immediately const response = new Response(JSON.stringify({ received: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
// Process in background ctx.waitUntil(processWebhookEvent(payload));
return response; } catch (error) { return new Response(JSON.stringify({ error: 'Processing failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }};2. Handle Errors Gracefully
Section titled “2. Handle Errors Gracefully”// Node.js - Proper error handlingapp.post('/webhook', async (req, res) => { try { // Verify signature if (!verifySignature(req.body.toString(), req.headers['zeltapay-timestamp'], req.headers['zeltapay-signature'])) { return res.status(401).json({ error: 'Invalid signature' }); }
const payload = JSON.parse(req.body); await processWebhookEvent(payload);
res.status(200).json({ received: true }); } catch (error) { console.error('Webhook processing error:', error);
// Return 5xx for retryable errors if (error.message.includes('database') || error.message.includes('network')) { return res.status(500).json({ error: 'Internal server error' }); }
// Return 4xx for non-retryable errors return res.status(400).json({ error: 'Bad request' }); }});3. Monitor Webhook Health
Section titled “3. Monitor Webhook Health”Track key metrics to ensure webhook reliability:
class WebhookMonitor { constructor() { this.metrics = { total: 0, success: 0, failure: 0, retry: 0 }; }
recordDelivery(attempt, success) { this.metrics.total++;
if (success) { this.metrics.success++; } else { this.metrics.failure++; }
if (attempt > 1) { this.metrics.retry++; }
this.logMetrics(); }
logMetrics() { const successRate = (this.metrics.success / this.metrics.total) * 100; const retryRate = (this.metrics.retry / this.metrics.total) * 100;
console.log('Webhook Metrics:', { total: this.metrics.total, successRate: `${successRate.toFixed(2)}%`, retryRate: `${retryRate.toFixed(2)}%`, failures: this.metrics.failure }); }}Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Webhook not receiving events
- Check webhook URL is correct and accessible
- Verify HTTPS is enabled
- Check firewall settings
- Test with webhook ping
Signature verification failing
- Check webhook secret is correct
- Ensure raw body is used (not parsed JSON)
- Verify timestamp format
- Check signature header format
Duplicate events
- Implement idempotency using eventId
- Check for race conditions
- Monitor delivery attempts
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 });}// 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') }); }
return new Response('Not found', { status: 404 }); }};Next Steps
Section titled “Next Steps”Now that you understand webhooks, explore these resources:
- Signature Verification Guide - Complete signature verification implementation
- Webhook Delivery Guide - Delivery mechanisms and retry logic
- Webhook Idempotency Guide - Prevent duplicate processing
- Webhook Events API - Event types and payloads
- Use Cases - See webhooks in action