Handling Webhooks
Webhooks allow you to receive real-time updates about payment events. This guide explains how to securely handle webhook notifications from lomi.
Setup
1. Configure Your Endpoint
Create a dedicated endpoint for webhook notifications:
import express from 'express';
import crypto from 'crypto';
const app = express();
// Use raw body parser for webhook signature verification
app.post('/webhooks',
express.raw({type: 'application/json'}),
handleWebhook
);
2. Verify Signatures
Always verify webhook signatures to ensure requests are from lomi:
function verifySignature(
payload: Buffer,
signature: string,
secret: string
): boolean {
const hmac = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(hmac)
);
}
async function handleWebhook(req: Request, res: Response) {
const signature = req.headers['lomi-signature'];
if (!signature || !verifySignature(
req.body,
signature as string,
process.env.WEBHOOK_SECRET
)) {
return res.status(400).json({
error: 'Invalid signature'
});
}
const event = JSON.parse(req.body.toString());
// Process the event
await processWebhookEvent(event);
// Return 200 quickly
res.json({ received: true });
}
Event Types
Payment Events
-
payment.created
if (event.type === 'payment.created') { const payment = event.data; await updateOrderStatus(payment.metadata.orderId, 'pending'); }
-
payment.succeeded
if (event.type === 'payment.succeeded') { const payment = event.data; await fulfillOrder(payment.metadata.orderId); }
-
payment.failed
if (event.type === 'payment.failed') { const payment = event.data; await notifyCustomer(payment.metadata.orderId, 'payment_failed'); }
Refund Events
-
refund.created
if (event.type === 'refund.created') { const refund = event.data; await updateRefundStatus(refund.id, 'pending'); }
-
refund.succeeded
if (event.type === 'refund.succeeded') { const refund = event.data; await completeRefund(refund.id); }
Best Practices
1. Quick Response
Respond quickly to webhook notifications:
async function handleWebhook(req: Request, res: Response) {
// Verify signature first
// Return 200 quickly
res.json({ received: true });
// Process event asynchronously
try {
await processWebhookEvent(event);
} catch (error) {
// Log error but don't affect response
console.error('Webhook processing error:', error);
}
}
2. Idempotency
Handle duplicate events gracefully:
async function processWebhookEvent(event: WebhookEvent) {
// Check if event was already processed
const processed = await db.webhookEvents.findOne({
eventId: event.id
});
if (processed) {
return; // Skip processing
}
// Process the event
await handleEvent(event);
// Mark as processed
await db.webhookEvents.create({
eventId: event.id,
type: event.type,
processedAt: new Date()
});
}
3. Error Handling
Implement robust error handling:
async function handleWebhook(req: Request, res: Response) {
try {
// Verify signature
// Parse event
const event = JSON.parse(req.body.toString());
// Return 200 quickly
res.json({ received: true });
// Process asynchronously
await processWebhookEvent(event);
} catch (error) {
console.error('Webhook error:', error);
// Don't expose error details
res.status(400).json({
error: 'Invalid webhook payload'
});
}
}
4. Logging
Implement comprehensive logging:
function logWebhookEvent(event: WebhookEvent) {
// Remove sensitive data
const sanitizedEvent = {
...event,
data: {
...event.data,
customer: '[REDACTED]'
}
};
console.log('Webhook received:', {
id: event.id,
type: event.type,
created: event.created,
data: sanitizedEvent.data
});
}
Testing Webhooks
Local Development
Use the CLI for local testing:
lomi webhook forward --url http://localhost:3000/webhooks
Test Events
Send test events using the CLI:
lomi webhook test --event payment.succeeded
Monitoring
1. View Recent Events
lomi webhook events list
2. Retry Failed Events
lomi webhook events retry <event_id>