> ## Documentation Index
> Fetch the complete documentation index at: https://lava.so/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when customers are created, updated, or deleted

Webhooks let you receive real-time notifications when events occur in Lava, such as new customers being created, wallet balances changing, or customers being deleted.

> [!NOTE]
>
> **Webhooks provide reliable backend processing.** While frontend callbacks offer instant UX feedback, webhooks ensure events are processed even when users close tabs or lose network connection.

## Setting Up a Webhook Endpoint

Create an API route to receive webhook events. Lava signs every request with HMAC SHA-256 via the `X-Webhook-Signature` header — always verify this before processing.

> [!WARNING]
>
> **Always verify signatures.** Without verification, malicious actors could send fake events to grant unauthorized access, trigger false balance alerts, or simulate deletions.

> [tab] Next.js
>
> ```typescript app/api/webhooks/lava/route.ts theme={null}
> import { NextRequest, NextResponse } from 'next/server';
> import crypto, { timingSafeEqual } from 'crypto';
>
> export async function POST(req: NextRequest) {
>   const body = await req.text();
>   const signature = req.headers.get('X-Webhook-Signature');
>
>   if (!signature || !verifySignature(body, signature)) {
>     return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
>   }
>
>   const payload = JSON.parse(body);
>
>   // Return 200 quickly, process in background
>   handleWebhookEvent(payload).catch(console.error);
>   return NextResponse.json({ received: true });
> }
>
> function verifySignature(body: string, signature: string): boolean {
>   const expected = crypto
>     .createHmac('sha256', process.env.LAVA_WEBHOOK_SECRET!)
>     .update(body)
>     .digest('hex');
>
>   const sigBuf = Buffer.from(signature, 'hex');
>   const expBuf = Buffer.from(expected, 'hex');
>   return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
> }
>
> async function handleWebhookEvent(payload: { event: string; data: any }) {
>   switch (payload.event) {
>     case 'customer.created':
>       // Store customer_id, send welcome email, enable access
>       break;
>     case 'customer.wallet.balance.updated':
>       // Check subscription credits, notify on low balance
>       break;
>     case 'customer.deleted':
>       // Revoke access, clean up customer_id
>       break;
>   }
> }
> ```

> [tab] Express
>
> ```typescript theme={null}
> import express from 'express';
> import crypto, { timingSafeEqual } from 'crypto';
>
> const app = express();
>
> // IMPORTANT: Use express.raw() for webhooks (not express.json())
> app.post('/api/webhooks/lava', express.raw({ type: 'application/json' }), async (req, res) => {
>   const signature = req.headers['x-webhook-signature'] as string;
>   const body = req.body.toString('utf8');
>
>   if (!signature || !verifySignature(body, signature)) {
>     return res.status(401).json({ error: 'Invalid signature' });
>   }
>
>   const payload = JSON.parse(body);
>
>   // Return 200 quickly, process in background
>   handleWebhookEvent(payload).catch(console.error);
>   res.json({ received: true });
> });
>
> function verifySignature(body: string, signature: string): boolean {
>   const expected = crypto
>     .createHmac('sha256', process.env.LAVA_WEBHOOK_SECRET!)
>     .update(body)
>     .digest('hex');
>
>   const sigBuf = Buffer.from(signature, 'hex');
>   const expBuf = Buffer.from(expected, 'hex');
>   return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
> }
> ```

### Register in Dashboard

1. Go to **Monetize > Webhooks** in the Lava dashboard
2. Click **"Add Endpoint"** and enter your webhook URL
3. Select events to receive
4. Copy the **Webhook Secret** and add to your environment:

```bash theme={null}
LAVA_WEBHOOK_SECRET=whs_live_xxxxxxxxxxxxxxxxxxxxxxxxxx
```

> [!NOTE]
>
> **Multiple endpoints supported.** You can register different URLs for different event types.

## Verifying Signatures

If you need a standalone verification function (e.g., for a shared utility), here it is in Node.js and Python:

> [tab] Node.js
>
> ```typescript theme={null}
> import crypto, { timingSafeEqual } from 'crypto';
>
> function verifyWebhookSignature(body: string, signature: string, secret: string): boolean {
>   const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
>   const sigBuf = Buffer.from(signature, 'hex');
>   const expBuf = Buffer.from(expected, 'hex');
>   return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
> }
> ```

> [tab] Python
>
> ```python theme={null}
> import hmac, hashlib
>
> def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
>     expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
>     return hmac.compare_digest(expected, signature)
> ```

## Event Types

All events deliver the same payload structure with a `customer` object:

```json theme={null}
{
  "event": "customer.created",
  "data": {
    "customer_id": "conn_xxxxx",
    "contact": {
      "phone": "+15551234567",
      "email": "user@example.com",
      "first_name": "Jane",
      "last_name": "Doe"
    },
    "subscription": {
      "subscription_id": "sub_xxxxx",
      "plan_id": "sc_xxxxx",
      "name": "Pro Plan",
      "status": "active"
    },
    "created_at": "2025-01-15T10:30:00Z"
  }
}
```

> [!NOTE]
>
> The top-level key is `event` (not `type`). The `subscription` field is `null` for customers without an active subscription.

### customer.created

Fired when a customer completes checkout and a new customer record is established.

**Use this to:**

* Store the `customer_id` in your database, linked to your internal user
* Map the customer by email or phone to your user record
* Send a welcome email or enable access to your service

### customer.wallet.balance.updated

Fired when a customer's wallet balance changes — after API usage is charged, a payment is made, or credits are added.

**Use this to:**

* Check subscription credits to gate features when credits run out
* Notify users when their balance is low
* Track spending patterns

### customer.deleted

Fired when a customer is deleted by the customer or merchant.

**Use this to:**

* Revoke access in your application
* Clean up the stored `customer_id` in your database

## Delivery Behavior

> [!TIP]
>
> Webhooks are delivered once with a **10-second timeout**. Return `200` immediately and process asynchronously to avoid timeouts. Implement idempotency using `customer_id` to handle any duplicate deliveries gracefully.

## Testing Locally

> [tab] ngrok (Recommended)
>
> Expose your local endpoint with a tunnel:
>
> ```bash theme={null}
> # Start your local server
> npm run dev
>
> # Expose port 3000
> ngrok http 3000
> ```
>
> Then register the ngrok URL in the Lava dashboard:
> `https://abc123.ngrok.io/api/webhooks/lava`

> [tab] Test Script
>
> Send a signed test event to your local endpoint:
>
> ```typescript scripts/test-webhook.ts theme={null}
> import crypto from 'crypto';
>
> const WEBHOOK_SECRET = 'your_webhook_secret';
> const WEBHOOK_URL = 'http://localhost:3000/api/webhooks/lava';
>
> const testEvent = {
>   event: 'customer.created',
>   data: {
>     customer_id: 'conn_test123',
>     contact: {
>       phone: '+15551234567',
>       email: 'test@example.com',
>       first_name: 'Jane',
>       last_name: 'Doe'
>     },
>     subscription: null,
>     created_at: new Date().toISOString()
>   }
> };
>
> const body = JSON.stringify(testEvent);
> const signature = crypto
>   .createHmac('sha256', WEBHOOK_SECRET)
>   .update(body)
>   .digest('hex');
>
> const response = await fetch(WEBHOOK_URL, {
>   method: 'POST',
>   headers: {
>     'Content-Type': 'application/json',
>     'X-Webhook-Signature': signature
>   },
>   body
> });
>
> console.log('Status:', response.status);
> console.log('Response:', await response.json());
> ```
>
> Run with: `npx tsx scripts/test-webhook.ts`

## Troubleshooting

> [accordion] Signature verification failing
>
> **Common causes:**
>
> * Using parsed JSON body instead of raw body string
> * Express middleware (`express.json()`) parsing body before verification
> * Incorrect webhook secret in environment variables
>
> **Solutions:**
>
> * Use `express.raw()` middleware for webhook routes
> * In Next.js, use `req.text()` to get the raw body
> * Verify `LAVA_WEBHOOK_SECRET` matches the value in your dashboard

> [accordion] Webhooks not being received
>
> * Webhook URL must be publicly accessible (not `localhost`)
> * Endpoint must return a `200` status code
> * Check for firewalls blocking inbound requests
> * Verify events are selected and endpoint is enabled in dashboard

> [accordion] Webhooks timing out
>
> * Return `200` immediately — don't block on heavy processing
> * Move long-running operations to a background job queue
> * Check that your endpoint responds within 10 seconds

> [accordion] Duplicate events received
>
> * Implement idempotency using `customer_id` + event type as a dedup key
> * Check if event was already processed before handling
> * Use a database unique constraint on the dedup key

## Next Steps

> [card] [Checkout](https://lava.so/docs/monetize/checkout.md)
>
> Set up checkout flow that triggers webhook events

> [card] [Forward Proxy & Authentication](https://lava.so/docs/gateway/forward-proxy.md)
>
> Generate forward tokens from webhook customer data


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.