> ## 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.

<Info>
  **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.
</Info>

## 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.
</Warning>

<Tabs>
  <Tab title="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>

  <Tab title="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);
    }
    ```
  </Tab>
</Tabs>

### 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
```

<Info>
  **Multiple endpoints supported.** You can register different URLs for different event types.
</Info>

## Verifying Signatures

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

<Tabs>
  <Tab title="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>

  <Tab title="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)
    ```
  </Tab>
</Tabs>

## 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.
</Note>

### 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.
</Tip>

## Testing Locally

<Tabs>
  <Tab title="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>

  <Tab title="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`
  </Tab>
</Tabs>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Signature verification failing" icon="shield-halved">
    **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>

  <Accordion title="Webhooks not being received" icon="inbox">
    * 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>

  <Accordion title="Webhooks timing out" icon="clock">
    * 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>

  <Accordion title="Duplicate events received" icon="copy">
    * 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
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Checkout" icon="credit-card" href="/monetize/checkout">
    Set up checkout flow that triggers webhook events
  </Card>

  <Card title="Forward Proxy & Authentication" icon="route" href="/gateway/forward-proxy">
    Generate forward tokens from webhook customer data
  </Card>
</CardGroup>
