Skip to content

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.

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"
}
}

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."
}

All events include these common fields:

FieldTypeDescription
eventIdstringUnique identifier for this event
typestringEvent type (e.g., “payment.success”, “webhook.ping”)
createdAtstringISO 8601 timestamp when the event was created
FieldTypeDescription
paymentLinkIdstringID of the payment link that was completed
accountIdstringID of the account that owns the payment link
transactionobjectTransaction details
customerobjectCustomer information
metadataobjectCustom metadata from the payment link
FieldTypeDescription
externalPaymentIdstringExternal payment processor ID
orderIdstringOrder identifier
amountintegerAmount in cents
currencystringCurrency code (e.g., “USD”)
paidAtstringISO 8601 timestamp when payment was completed
paymentMethodstringPayment method used
paymentProviderstringPayment provider name
conceptstringDescription of what was paid for
paidTostringName of the business receiving payment
FieldTypeDescription
customerNamestringName of the customer
customerEmailstringEmail address of the customer

Every webhook request includes these headers:

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
User-AgentZelta Pay webhook identifierzeltapay-webhook/1.0
Zeltapay-Event-IdUnique event identifierevt_1234567890abcdef
Zeltapay-Event-TypeEvent typepayment.success
Zeltapay-TimestampUnix timestamp1640995200
Zeltapay-SignatureHMAC signaturet=1640995200, v1=abc123...
Zeltapay-Delivery-AttemptAttempt number1

When you receive a payment.success event:

  1. Verify the signature to ensure authenticity
  2. Check for duplicate events using the eventId
  3. Update your database with the payment information
  4. Send confirmation emails to customers
  5. Trigger fulfillment processes for orders
  6. Update order status to “paid”
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 });
});
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);
}

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);
}

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;
}
}

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)
});
}

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;
}
}

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

Enable debug logging to troubleshoot:

function debugWebhookEvent(req) {
console.log('Webhook debug info:', {
headers: req.headers,
body: req.body.toString(),
timestamp: new Date().toISOString()
});
}