DocsAdvanced guidesHandling Webhooks

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

  1. payment.created

    if (event.type === 'payment.created') {
      const payment = event.data;
      await updateOrderStatus(payment.metadata.orderId, 'pending');
    }
  2. payment.succeeded

    if (event.type === 'payment.succeeded') {
      const payment = event.data;
      await fulfillOrder(payment.metadata.orderId);
    }
  3. payment.failed

    if (event.type === 'payment.failed') {
      const payment = event.data;
      await notifyCustomer(payment.metadata.orderId, 'payment_failed');
    }

Refund Events

  1. refund.created

    if (event.type === 'refund.created') {
      const refund = event.data;
      await updateRefundStatus(refund.id, 'pending');
    }
  2. 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>

Next Steps