Skip to main content
Webhooks let you receive real-time notifications when events occur in Lava, such as new connections being created, wallet balances changing, or connections being deleted.
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.
Always verify signatures. Without verification, malicious actors could send fake events to grant unauthorized access, trigger false balance alerts, or simulate deletions.
app/api/webhooks/lava/route.ts
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 'connection.created':
      // Store connection_id, send welcome email, enable access
      break;
    case 'connection.wallet.balance.updated':
      // Check has_lava_credit_balance, notify on low balance
      break;
    case 'connection.deleted':
      // Revoke access, clean up connection_id
      break;
  }
}

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:
LAVA_WEBHOOK_SECRET=whs_live_xxxxxxxxxxxxxxxxxxxxxxxxxx
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:
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);
}

Event Types

All events deliver the same payload structure with a connection object:
{
  "event": "connection.created",
  "data": {
    "connection_id": "conn_xxxxx",
    "has_lava_credit_balance": true,
    "customer": {
      "phone": "+15551234567",
      "email": "user@example.com",
      "first_name": "Jane",
      "last_name": "Doe"
    },
    "subscription": {
      "subscription_id": "sub_xxxxx",
      "subscription_config_id": "sc_xxxxx",
      "name": "Pro Plan",
      "status": "active"
    },
    "created_at": "2025-01-15T10:30:00Z"
  }
}
The top-level key is event (not type). The subscription field is null for connections without an active subscription.

connection.created

Fired when a customer completes checkout and a new connection is established. Use this to:
  • Store the connection_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

connection.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 has_lava_credit_balance to gate features when credits run out
  • Notify users when their balance is low
  • Track spending patterns

connection.deleted

Fired when a connection is deleted by the customer or merchant. Use this to:
  • Revoke access in your application
  • Clean up the stored connection_id in your database

Delivery Behavior

Webhooks are delivered once with a 10-second timeout. Return 200 immediately and process asynchronously to avoid timeouts. Implement idempotency using connection_id to handle any duplicate deliveries gracefully.

Testing Locally

Troubleshooting

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
  • 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
  • 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
  • Implement idempotency using connection_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