Webhook Events
Zelta Pay sends webhook events to notify your application about important changes. This API reference documents all available event types and their payloads.
Event Types
Section titled “Event Types”Payment Success Event
Section titled “Payment Success Event”Sent when a customer successfully completes a payment.
Event Type: payment.success
Payload:
{ "eventId": "evt_1234567890abcdef", "type": "payment.success", "paymentLinkId": "pl_1234567890abcdef", "accountId": "acc_1234567890abcdef", "createdAt": "2024-01-15T10:30:00.000Z", "transaction": { "externalPaymentId": "ext_abc123", "orderId": "ORD-12345-xyz", "amount": 2500, "currency": "USD", "paidAt": "2024-01-15T10:30:00.000Z", "paymentMethod": "credit_card", "paymentProvider": "nmi", "concept": "Consulting Service", "paidTo": "Acme Inc" }, "customer": { "customerName": "John Doe", "customerEmail": "john@example.com" }, "metadata": { "orderId": "ORD-001", "service": "consulting" }}Webhook Ping Event
Section titled “Webhook Ping Event”Sent when you test your webhook endpoint.
Event Type: webhook.ping
Payload:
{ "eventId": "evt_1234567890abcdef", "type": "webhook.ping", "ok": true, "message": "Hello from Zeltapay! Your endpoint is reachable.", "sentAt": "2024-01-15T10:30:00.000Z", "description": "This is a ping payload sent to verify your webhook endpoint. If you received this with a 2xx status, everything is working."}Event Fields
Section titled “Event Fields”Common Fields
Section titled “Common Fields”All events include these common fields:
| Field | Type | Description |
|---|---|---|
eventId | string | Unique identifier for this event |
type | string | Event type (e.g., “payment.success”, “webhook.ping”) |
createdAt | string | ISO 8601 timestamp when the event was created |
Payment Success Event Fields
Section titled “Payment Success Event Fields”| Field | Type | Description |
|---|---|---|
paymentLinkId | string | ID of the payment link that was completed |
accountId | string | ID of the account that owns the payment link |
transaction | object | Transaction details |
customer | object | Customer information |
metadata | object | Custom metadata from the payment link |
Transaction Object
Section titled “Transaction Object”| Field | Type | Description |
|---|---|---|
externalPaymentId | string | External payment processor ID |
orderId | string | Order identifier |
amount | integer | Amount in cents |
currency | string | Currency code (e.g., “USD”) |
paidAt | string | ISO 8601 timestamp when payment was completed |
paymentMethod | string | Payment method used |
paymentProvider | string | Payment provider name |
concept | string | Description of what was paid for |
paidTo | string | Name of the business receiving payment |
Customer Object
Section titled “Customer Object”| Field | Type | Description |
|---|---|---|
customerName | string | Name of the customer |
customerEmail | string | Email address of the customer |
Event Headers
Section titled “Event Headers”Every webhook request includes these headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
User-Agent | Zelta Pay webhook identifier | zeltapay-webhook/1.0 |
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 |
Event Processing
Section titled “Event Processing”Handling Payment Success Events
Section titled “Handling Payment Success Events”When you receive a payment.success event:
- Verify the signature to ensure authenticity
- Check for duplicate events using the
eventId - Update your database with the payment information
- Send confirmation emails to customers
- Trigger fulfillment processes for orders
- Update order status to “paid”
Example Handlers
Section titled “Example Handlers”Node.js Handler
Section titled “Node.js Handler”app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['zeltapay-signature']; const timestamp = req.headers['zeltapay-timestamp']; const eventId = req.headers['zeltapay-event-id']; const eventType = req.headers['zeltapay-event-type'];
// Verify signature if (!verifySignature(req.body.toString(), timestamp, signature)) { return res.status(401).json({ error: 'Invalid signature' }); }
const payload = JSON.parse(req.body);
// Check for duplicate events if (processedEvents.has(eventId)) { console.log(`Duplicate event ignored: ${eventId}`); return res.status(200).json({ received: true }); }
// Process based on event type switch (eventType) { case 'payment.success': handlePaymentSuccess(payload); break; case 'webhook.ping': console.log('Webhook ping received:', payload.message); break; default: console.log('Unknown event type:', eventType); }
// Mark event as processed processedEvents.add(eventId);
res.status(200).json({ received: true });});Cloudflare Workers Handler
Section titled “Cloudflare Workers Handler”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 signature = request.headers.get('zeltapay-signature'); const timestamp = request.headers.get('zeltapay-timestamp'); const eventId = request.headers.get('zeltapay-event-id'); const eventType = request.headers.get('zeltapay-event-type');
// Get raw body const rawBody = await request.text();
// Verify signature if (!await verifySignature(rawBody, timestamp, signature, env.WEBHOOK_SECRET)) { return new Response(JSON.stringify({ error: 'Invalid signature' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); }
const payload = JSON.parse(rawBody);
// Check for duplicate events (using KV store for persistence) const existingEvent = await env.WEBHOOK_EVENTS.get(eventId); if (existingEvent) { console.log(`Duplicate event ignored: ${eventId}`); return new Response(JSON.stringify({ received: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }
// Process based on event type switch (eventType) { case 'payment.success': await handlePaymentSuccess(payload); break; case 'webhook.ping': console.log('Webhook ping received:', payload.message); break; default: console.log('Unknown event type:', eventType); }
// Mark event as processed (store in KV with 24h TTL) await env.WEBHOOK_EVENTS.put(eventId, 'processed', { expirationTtl: 86400 });
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 verifySignature(rawBody, timestamp, signature, secret) { // Implementation details in signature verification guide return true; // Placeholder}
async function handlePaymentSuccess(payload) { // Process payment success event console.log('Payment completed:', payload.transaction.amount);}Best Practices
Section titled “Best Practices”1. Idempotency
Section titled “1. Idempotency”Always check for duplicate events using the eventId:
const processedEvents = new Set();
function isEventProcessed(eventId) { return processedEvents.has(eventId);}
function markEventProcessed(eventId) { processedEvents.add(eventId);}2. Error Handling
Section titled “2. Error Handling”Handle different event types gracefully:
function handleWebhookEvent(payload) { try { switch (payload.type) { case 'payment.success': return handlePaymentSuccess(payload); case 'webhook.ping': return handleWebhookPing(payload); default: console.log('Unknown event type:', payload.type); return { success: true, message: 'Event type not handled' }; } } catch (error) { console.error('Error handling webhook event:', error); throw error; }}3. Logging
Section titled “3. Logging”Log all webhook events for debugging:
function logWebhookEvent(eventId, eventType, payload) { console.log('Webhook event received:', { eventId, eventType, timestamp: new Date().toISOString(), payload: JSON.stringify(payload, null, 2) });}4. Database Integration
Section titled “4. Database Integration”Store webhook events in your database:
async function storeWebhookEvent(eventId, eventType, payload) { try { await db.webhookEvents.create({ eventId, eventType, payload: JSON.stringify(payload), processed: false, createdAt: new Date() }); } catch (error) { console.error('Error storing webhook event:', error); throw error; }}Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Event not received
- Check webhook URL is correct and accessible
- Verify HTTPS is enabled
- Check firewall settings
- Test with webhook ping
Duplicate events
- Implement idempotency using eventId
- Check for race conditions
- Monitor delivery attempts
Event processing errors
- Check signature verification
- Validate payload structure
- Handle missing fields gracefully
Debug Mode
Section titled “Debug Mode”Enable debug logging to troubleshoot:
function debugWebhookEvent(req) { console.log('Webhook debug info:', { headers: req.headers, body: req.body.toString(), timestamp: new Date().toISOString() });}Related Documentation
Section titled “Related Documentation”- Webhooks Guide - Complete webhook setup guide
- Signature Verification Guide - Complete signature verification implementation
- Webhook Delivery API - Delivery mechanisms and retry logic
- Webhook Idempotency API - Prevent duplicate processing
- Use Cases - See webhooks in action