DocsAdvanced guidesHandling Webhooks

Handling webhooks

Webhooks provide real-time updates about events in your lomi. account. This guide explains how to securely receive and process these notifications.

For a general introduction and setup guide, see Setting up webhooks.

Setup summary

1. Configure your endpoint

Ensure you have a dedicated HTTPS endpoint ready to receive POST requests with a JSON body.

Basic Express setup for webhooks
import express from 'express';
import crypto from 'crypto';
 
const app = express();
 
// Define your webhook handling function
async function handleWebhook(req: express.Request, res: express.Response) {
  const LOMI_WEBHOOK_SECRET = process.env.LOMI_WEBHOOK_SECRET;
  if (!LOMI_WEBHOOK_SECRET) {
      console.error('Webhook secret is not configured.');
      return res.status(500).send('Webhook configuration error');
  }
 
  // Verify signature (implementation below)
  const signature = req.headers['x-lomi-signature'] as string;
  if (!signature || !verifySignature(req.body, signature, LOMI_WEBHOOK_SECRET)) {
    return res.status(400).send('Invalid signature');
  }
 
  // Respond quickly to acknowledge receipt
  res.status(200).json({ received: true });
 
  // Process the event asynchronously
  const event = JSON.parse(req.body.toString());
  try {
    await processWebhookEvent(event);
  } catch (error) {
    console.error('Error processing webhook event:', error);
    // Log the error, but don't fail the response to lomi.
  }
}
 
// Use express.raw() middleware to access the raw body for signature verification
app.post('/your-webhook-endpoint',
  express.raw({ type: 'application/json' }),
  handleWebhook
);
 
// Your signature verification function (see below)
function verifySignature(payload: Buffer, signature: string, secret: string): boolean {
  // ... implementation ...
  return true; // Placeholder
}
 
// Your event processing logic
async function processWebhookEvent(event: any): Promise<void> {
  console.log(`Processing event: ${event.id}, Type: ${event.event}`);
  // Add your business logic here based on event.event
}
 
// Start the server...

2. Verify signatures

Always verify the X-Lomi-Signature header to ensure the request is genuinely from lomi. and hasn’t been tampered with.

Webhook signature verification function
import crypto from 'crypto';
 
function verifySignature(
  payload: Buffer, // Raw request body (Buffer)
  signatureHeader: string,
  secret: string
): boolean {
  if (!payload || !signatureHeader || !secret) {
    return false;
  }
 
  try {
    const hmac = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
 
    // Use timing-safe comparison
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader),
      Buffer.from(hmac)
    );
  } catch (error) {
    console.error('Error during signature verification:', error);
    return false;
  }
}

Processing events

Once the signature is verified, you can safely process the event payload.

Processing webhook events
interface LomiWebhookEvent {
  id: string;
  event: string; // e.g., 'PAYMENT_SUCCEEDED'
  timestamp: string;
  data: any; // Structure depends on the event type
  lomi_environment: 'live' | 'test';
}
 
async function processWebhookEvent(event: LomiWebhookEvent): Promise<void> {
  // Optional: Check if event ID has already been processed for idempotency
  if (await hasEventBeenProcessed(event.id)) {
    console.log(`Event ${event.id} already processed. Skipping.`);
    return;
  }
 
  console.log(`Processing event: ${event.id}, Type: ${event.event}`);
 
  switch (event.event) {
    case 'PAYMENT_SUCCEEDED':
      const transaction = event.data; // Contains transaction object
      console.log(`Payment succeeded for transaction: ${transaction.transaction_id}`);
      // Example: Fulfill order, grant access, update database
      // await fulfillOrder(transaction.metadata?.order_id, transaction);
      break;
 
    case 'PAYMENT_FAILED':
      const failedTxn = event.data;
      console.log(`Payment failed for transaction: ${failedTxn.transaction_id}`);
      // Example: Notify customer, update order status to failed
      // await handleFailedPayment(failedTxn.metadata?.order_id, failedTxn);
      break;
 
    case 'SUBSCRIPTION_CREATED':
      const subscription = event.data;
      console.log(`Subscription created: ${subscription.subscription_id}`);
      // Example: Provision service for new subscription
      break;
 
    // Add cases for other events you subscribe to...
 
    default:
      console.warn(`Unhandled event type: ${event.event}`);
  }
 
  // Optional: Mark event as processed
  await markEventAsProcessed(event.id);
}
 
// Placeholder functions for idempotency checks (implement with your database/cache)
async function hasEventBeenProcessed(eventId: string): Promise<boolean> {
  // Check your storage if eventId exists
  return false; // Replace with actual check
}
async function markEventAsProcessed(eventId: string): Promise<void> {
  // Store eventId in your storage
}

Best practices

1. Respond quickly

Acknowledge webhook receipt by returning a 2xx status code (e.g., 200) immediately. Defer any complex processing or external API calls to a background job queue.

Responding quickly and processing asynchronously
async function handleWebhook(req: express.Request, res: express.Response) {
  // ... (Verify signature) ...
  if (!isValidSignature) {
    return res.status(400).send('Invalid signature');
  }
 
  // Acknowledge receipt immediately
  res.status(200).json({ received: true });
 
  // Add event to a background queue for processing
  const event = JSON.parse(req.body.toString());
  backgroundQueue.add('process-webhook', event);
}

2. Handle duplicates (idempotency)

Network issues or retries can cause duplicate webhook delivery. Ensure your system handles this gracefully by making your event processing idempotent.

  • Check Event ID: Store the id (evt_...) of processed events. Before processing a new event, check if its ID has already been processed. If so, skip processing.
  • Database Constraints: Use unique constraints in your database where appropriate (e.g., on an order update based on the transaction ID) to prevent duplicate operations at the data layer.
Idempotency check example
async function processWebhookEvent(event: LomiWebhookEvent): Promise<void> {
  const isProcessed = await database.checkIfEventProcessed(event.id);
  if (isProcessed) {
    console.log(`Event ${event.id} is a duplicate, skipping.`);
    return;
  }
 
  // ... process the event ...
 
  await database.markEventAsProcessed(event.id);
}

3. Error handling

Implement robust error handling within your processWebhookEvent function.

  • Log Errors: Log detailed errors encountered during processing.
  • Retry Logic (Internal): For transient errors during processing (e.g., temporary database unavailability), consider internal retry logic within your background job handler.
  • Monitoring: Monitor your webhook endpoint for failures and your background queue for processing errors.
  • Do Not Fail the 200 OK Response: Even if your internal processing fails later, ensure you have already sent the 200 OK response to lomi.. lomi. only cares about the successful delivery acknowledgment.

4. Logging

Log key information for debugging:

  • Log receipt of webhook events (including the event ID and type).
  • Log the outcome of signature verification.
  • Log the start and end of event processing.
  • Log any errors during processing with relevant context (but avoid logging the full raw payload or sensitive data directly unless necessary and properly secured).
Example logging within handler
async function handleWebhook(req: express.Request, res: express.Response) {
  const eventId = JSON.parse(req.body.toString())?.id || 'unknown';
  console.log(`Received webhook request for event ID (potential): ${eventId}`);
 
  // ... (Verify signature) ...
  if (!isValidSignature) {
    console.warn(`Invalid signature for event ID: ${eventId}`);
    return res.status(400).send('Invalid signature');
  }
  console.log(`Signature verified for event ID: ${eventId}`);
 
  res.status(200).json({ received: true });
 
  // ... (Process asynchronously) ...
}

Testing webhooks

Refer to the Testing guide for details on using the lomi. Dashboard or CLI to send test events to your local endpoint.

Monitoring

Use the lomi. Dashboard (Developers -> Webhooks) to monitor delivery attempts, view recent events, check response codes from your endpoint, and manually retry failed deliveries.

Next steps