Skip to main content

What You’ll Build

In this quickstart, you’ll set up a complete monetization flow for your AI service. By the end, you’ll have:
  • A Stripe Connect account configured for payouts
  • A product with custom pricing rules
  • A functioning checkout flow to onboard customers
  • Customer wallet connections for billing
  • Forward tokens generated for API authentication
  • Revenue tracking visible in your dashboard
Prerequisites: You need a Lava merchant account and have completed the Build Quickstart. This tutorial builds on that foundation to add customer billing.

Step 1: Connect Your Stripe Account

Before you can earn revenue, Lava needs a way to pay you. We use Stripe Connect Express for secure, automated payouts.

Start Stripe Onboarding

  1. Open your dashboard at www.lava.so/dashboard
  2. Navigate to Monetize > Billing in the sidebar
  3. Click “Connect Stripe Account”
  4. Complete the Stripe onboarding form:
    • Business details (name, type, address)
    • Bank account for payouts
    • Tax information (EIN or SSN)
  5. Submit and wait for approval (usually instant)
Already have a Stripe account? You can link your existing Stripe account during onboarding. Lava creates a Connect Express account that keeps your revenue separate from other Stripe activity.

Verify Connection

After completing onboarding, you’ll see:
  • Payout Schedule: When you’ll receive funds (typically daily or weekly)
  • Account Status: “Active” when ready to accept payments
  • Balance: Current pending payout amount
Stripe handles all compliance, tax reporting, and payment processing. Lava never holds your funds—payouts go directly to your bank account on the schedule you configure.

Step 2: Create Your First Plan

Plans define how customers are billed on a recurring basis. Let’s create a subscription plan.

Via Dashboard

  1. Navigate to Monetize > Plans in the sidebar
  2. Click “New Plan”
  3. Configure your plan:
Plan Settings:
  • Name: “Pro Plan” (customers will see this)
  • Monthly Amount: $25.00 (charged each billing cycle)
  • Rollover: Choose whether unused balance carries over to the next month
  1. Click “Create Plan”
  2. Copy the Plan ID (looks like subconf_xxxxxxxxxxxxx)
What’s a Plan ID? It’s the identifier you’ll use when creating checkout sessions. You can create multiple plans for different pricing tiers (e.g., “Starter” at 10/mo,"Pro"at10/mo, "Pro" at 25/mo, “Enterprise” at $100/mo).

Via SDK

import { Lava } from '@lavapayments/nodejs';

const lava = new Lava(process.env.LAVA_SECRET_KEY!, {
  apiVersion: '2025-04-28.v1'
});

const plan = await lava.subscriptions.createConfig({
  name: 'Pro Plan',
  period_amount: '25.00',  // $25 per month
  rollover_type: 'none'    // or 'full' to carry over unused balance
});

console.log('Plan created:', plan.subscription_config_id);

Step 3: Install the Checkout Component

Lava provides a React component that handles wallet creation, phone verification, and payment—all in one embedded flow.

Installation

npm install @lavapayments/checkout

Component Overview

The <LavaCheckout> component provides:
  • Phone verification via SMS OTP (Twilio)
  • Wallet creation for new users OR linking existing wallets
  • Stripe payment integration for adding funds
  • Connection creation linking the wallet to your merchant account
  • Success callback with connection credentials for API calls
The checkout component is fully customizable with your branding, custom amounts, and redirect URLs. See the Checkout Integration Guide for advanced configuration.

Step 4: Create a Checkout Session (Backend)

Before embedding the checkout component, you need to create a session on your backend. This ensures security and lets you configure checkout behavior.

Next.js API Route Example

// pages/api/checkout/create-session.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Lava } from '@lavapayments/nodejs';

const lava = new Lava(process.env.LAVA_SECRET_KEY!, {
  apiVersion: '2025-04-28.v1'
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { userId } = req.body;

  try {
    const session = await lava.checkoutSessions.create({
      checkout_mode: 'subscription',
      origin_url: process.env.NEXT_PUBLIC_BASE_URL!,
      subscription_config_id: 'subconf_your_plan_id', // Your Plan ID from Step 2
      reference_id: userId  // Links this connection to your user
    });

    res.status(200).json({
      checkoutSessionToken: session.checkout_session_token
    });
  } catch (error) {
    console.error('Checkout session creation failed:', error);
    res.status(500).json({ error: 'Failed to create checkout session' });
  }
}

Express.js Example

import express from 'express';
import { Lava } from '@lavapayments/nodejs';

const app = express();
const lava = new Lava(process.env.LAVA_SECRET_KEY!, { apiVersion: '2025-04-28.v1' });

app.post('/api/checkout/create-session', async (req, res) => {
  const { userId } = req.body;

  try {
    const session = await lava.checkoutSessions.create({
      checkout_mode: 'subscription',
      origin_url: process.env.BASE_URL!,
      subscription_config_id: 'subconf_your_plan_id',
      reference_id: userId
    });

    res.json({
      checkoutSessionToken: session.checkout_session_token
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to create checkout session' });
  }
});
Session secrets are single-use and expire after 30 minutes. Create a new session for each checkout attempt. Sessions are stored in Redis for fast access during the checkout flow.

Step 5: Embed the Checkout (Frontend)

Now trigger the checkout flow in your React application using the useLavaCheckout hook.

Basic Integration

'use client';  // Next.js App Router

import { useLavaCheckout } from '@lavapayments/checkout';

export function PricingPage({ userId }: { userId: string }) {
  const { open } = useLavaCheckout({
    onSuccess: async ({ connectionId, checkoutSessionId }) => {
      // Save the connection to your database
      await fetch('/api/save-connection', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, connectionId })
      });

      // Redirect to dashboard
      window.location.href = '/dashboard?checkout=success';
    },
    onCancel: ({ checkoutSessionId }) => {
      console.log('Checkout cancelled:', checkoutSessionId);
    },
    onError: ({ error }) => {
      console.error('Checkout error:', error);
    }
  });

  async function handleCheckoutClick() {
    // Call your backend to create a session
    const response = await fetch('/api/checkout/create-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId })
    });

    const { checkoutSessionToken } = await response.json();

    // Open the checkout modal
    open(checkoutSessionToken);
  }

  return (
    <button onClick={handleCheckoutClick}>
      Subscribe Now
    </button>
  );
}
The useLavaCheckout hook returns an open function that displays a fullscreen checkout modal when called with a checkout session token.
Testing the checkout flow? Use Stripe test mode with test card 4242 4242 4242 4242. The checkout component automatically detects your environment and uses test mode when appropriate.

Step 6: Handle Checkout Completion

When a user completes checkout, save the connection ID to your database. You’ll need it to retrieve the connection secret for generating forward tokens.

Frontend Callback

The onSuccess callback receives the connection ID immediately:
onSuccess: async ({ connectionId, checkoutSessionId }) => {
  // Save the connectionId linked to your user
  await fetch('/api/users/save-connection', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userId: currentUser.id,
      connectionId: connectionId
    })
  });

  // Redirect to success page
  window.location.href = '/dashboard?checkout=success';
}
The callback provides connectionId but not connection_secret. To generate forward tokens, retrieve the full connection from your backend using lava.connections.retrieve(connectionId).

Webhook Handler (Reliable Backup)

Set up a webhook handler to capture checkout events reliably (handles network failures, closed tabs, etc.):
// pages/api/webhooks/lava.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

// Verify webhook signature using HMAC SHA-256
function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const signature = req.headers['x-lava-signature'] as string;
  const payload = JSON.stringify(req.body);

  // Verify webhook signature (IMPORTANT for security)
  const isValid = verifyWebhookSignature(
    payload,
    signature,
    process.env.LAVA_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = req.body;

  // Handle different event types
  switch (event.type) {
    case 'checkout.completed':
      // Save connection to your database
      await saveConnection({
        referenceId: event.data.reference_id,  // Your userId from Step 4
        connectionId: event.data.connection_id
      });
      break;

    case 'connection.balance.low':
      // Notify user to add funds (optional)
      console.log('Low balance:', event.data.connection_id);
      break;
  }

  res.status(200).json({ received: true });
}

// Helper function
async function saveConnection(data: { referenceId: string; connectionId: string }) {
  // Save to your database (Prisma, Drizzle, etc.)
  // await db.connections.create({ data });
}
Always verify webhook signatures! This prevents attackers from sending fake webhook events to your endpoint. Use HMAC SHA-256 with timing-safe comparison.

Webhook Configuration

  1. Navigate to Monetize > Webhooks in the dashboard
  2. Click “Create Webhook”
  3. Enter your webhook URL: https://yourdomain.com/api/webhooks/lava
  4. Select events: checkout.completed, connection.wallet.balance.updated
  5. Copy the webhook secret and add to your .env:
LAVA_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

Step 7: Generate Forward Tokens

Now that you have a connection, generate forward tokens for your customers to use in API calls.
import { Lava } from '@lavapayments/nodejs';

const lava = new Lava(process.env.LAVA_SECRET_KEY!, {
  apiVersion: '2025-04-28.v1'
});

// First, retrieve the connection to get the connection_secret
const connection = await lava.connections.retrieve(connectionId);

// Generate a forward token for a customer
const forwardToken = lava.generateForwardToken({
  connection_secret: connection.connection_secret,
  product_secret: 'prod_your_product_secret'  // From Monetize > Products
});

// Return to customer (store in their account or send via API)
return {
  forwardToken: forwardToken,
  instructions: 'Use this token in the Authorization header for AI requests'
};
Where does product_secret come from? Create a Product (meter) in Monetize > Products to define your pricing rules (e.g., $0.05 per 1K tokens). The product_secret applies these pricing rules to requests. Plans define how much customers pay monthly; Products define how usage is priced.

Manual Generation (Advanced)

If you’re not using Node.js, you can generate tokens manually:
# Python example
import base64
import json

def generate_forward_token(secret_key, connection_secret, product_secret):
    token_data = {
        "secret_key": secret_key,
        "connection_secret": connection_secret,
        "product_secret": product_secret,
        "provider_key": None
    }
    return base64.b64encode(json.dumps(token_data).encode()).decode()

# Usage
token = generate_forward_token(
    secret_key='sk_live_xxxxx',
    connection_secret='conn_sec_xxxxx',
    product_secret='prod_xxxxx'
)

Distributing Tokens to Customers

Once generated, customers use forward tokens in their API calls:
const response = await fetch('https://api.lavapayments.com/v1/forward?u=https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${forwardToken}`
  },
  body: JSON.stringify({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: 'Hello!' }]
  })
});
Security best practice: Never expose your secret key to customers. Only distribute forward tokens, which are scoped to specific connections and products. Customers cannot access other connections or modify pricing with a forward token.

Step 8: Track Revenue in Your Dashboard

Monitor your revenue, usage, and customer activity in real-time.

Revenue Dashboard

Navigate to Monetize > Revenue to see: Overview Metrics:
  • Total Revenue: Lifetime earnings from all customers
  • Active Connections: Number of customers currently using your service
  • This Month’s Revenue: Current billing period earnings
  • Average Revenue Per User (ARPU)
Revenue Breakdown:
  • By Connection: See which customers generate the most revenue
  • By Product: Compare performance of different pricing tiers
  • By Time Period: Daily, weekly, monthly trends

Usage Analytics

Navigate to Monetize > Usage for detailed insights: Metrics Available:
  • Request Volume: Total API calls per connection
  • Token Usage: Input/output token consumption
  • Cost Breakdown: Base costs, fees, service charges
  • Model Distribution: Which AI models customers use most
Filtering Options:
  • Filter by date range, connection, product, or metadata
  • Export to CSV for external analysis
  • Create custom views for reporting

Connection Management

Navigate to Monetize > Connections to:
  • View All Connections: See every customer wallet linked to your merchant account
  • Connection Status: “ok” (funded), “limited” (low balance), “disabled” (blocked)
  • Balance Information: Current wallet balance per connection
  • Usage History: Request logs and costs for each connection
  • Reference IDs: Track customers using your internal user IDs
Set custom reference IDs in the checkout session creation to link Lava connections to your existing user system. This makes it easy to match revenue to your customer records.

Validation Checklist

Before going to production, verify everything works:
  • Stripe Connected: Payout account active and verified
  • Product Created: Pricing configured and product secret saved
  • Checkout Works: Test user can complete phone verification and payment
  • Webhook Configured: Endpoint receiving checkout.completed events
  • Forward Tokens Generated: SDK correctly creates tokens from connection secrets
  • API Requests Working: Test request with forward token succeeds
  • Revenue Tracking: Dashboard shows usage and costs accurately
  • Balance Deduction: Wallet balance decreases after requests
Test thoroughly before production! Use Stripe test mode and create test connections to verify the complete flow. Check that webhook signatures are validated correctly to prevent security vulnerabilities.

Troubleshooting

Possible causes:
  • Incomplete business information
  • Bank account verification failed
  • Tax ID (EIN/SSN) mismatch
Solution:
  1. Check your email for Stripe verification requests
  2. Complete any pending requirements in the Stripe dashboard
  3. Verify your bank account details are correct
  4. Contact Stripe support if issues persist
Common issue: If you already have a Stripe account, make sure you’re linking it correctly during onboarding (not creating a duplicate).
Possible causes:
  • Session secret expired (30-minute limit)
  • Invalid session secret format
  • Missing @lavapayments/checkout package
Solution:
  1. Verify the package is installed: npm list @lavapayments/checkout
  2. Check that sessionSecret is valid and not expired
  3. Create a new session if it’s been longer than 30 minutes
  4. Inspect browser console for errors
Debug checklist:
  • Session secret starts with cs_ prefix
  • Backend API returns valid JSON with sessionSecret field
  • React component is mounted in a client component ('use client' directive)
Possible causes:
  • Incorrect webhook URL configuration
  • Firewall blocking Lava’s servers
  • Webhook secret mismatch in signature verification
Solution:
  1. Verify URL is publicly accessible (use webhook.site to test)
  2. Check webhook logs in Monetize > Webhooks dashboard
  3. Ensure LAVA_WEBHOOK_SECRET matches the secret in dashboard
  4. Test locally using ngrok or similar tunneling tool
Testing locally:
# Use ngrok to expose localhost
ngrok http 3000

# Update webhook URL to ngrok HTTPS URL
https://abc123.ngrok.io/api/webhooks/lava
Retry logic: Lava automatically retries failed webhooks with exponential backoff (up to 3 attempts). Check the dashboard for retry logs.
Possible causes:
  • Incorrectly generated token (wrong format)
  • Expired or revoked secret key
  • Connection secret invalid or deleted
Solution:
  1. Verify token format: base64(secretKey.connectionSecret.productSecret)
  2. Check that secret key is active in Build > Secrets
  3. Verify connection exists in Monetize > Connections
  4. Regenerate token using SDK to ensure correct encoding
Test token manually:
// Decode to verify contents
const decoded = Buffer.from(forwardToken, 'base64').toString();
console.log(decoded);  // Should show: sk_xxx.conn_sec_xxx.prod_xxx
Common mistake: Forgetting to include the product secret when you have multiple products configured.
Possible causes:
  • Stripe payment succeeded but webhook not processed
  • Database race condition with concurrent requests
  • Balance transfer settlement delay
Solution:
  1. Check Stripe dashboard for successful payment
  2. Verify checkout.completed webhook was received and processed
  3. Look for errors in webhook handler logs
  4. Check balance manually in Monetize > Connections
Manual balance check: Navigate to the connection detail page and refresh. If Stripe shows payment success but Lava doesn’t, check for webhook processing errors in your server logs.Autopay alternative: Enable autopay to automatically top up when balance is low, preventing service interruption.
Possible causes:
  • Product secret not included in forward token
  • Tiered pricing configuration incorrect
  • Billing basis mismatch (input-output vs output-only)
Solution:
  1. Verify forward token includes product secret as third component
  2. Check product configuration in dashboard (Monetize > Products)
  3. Review request logs for correct usage calculation
  4. Test with simple fixed fee before adding tiers
Debugging pricing:
  • Check dashboard request logs to see calculated cost
  • Compare against expected calculation: tokens × tier_rate
  • Verify billingBasis matches your expectations (input+output vs output-only)
Example issue: Billing basis set to output-only but expecting to charge for prompts. Change to input-output to charge for both.

What’s Next?

Congratulations! You’ve set up a complete monetization flow. Here are recommended next steps:

How Lava Monetize Works

Understanding the monetization flow helps you troubleshoot and optimize:
  1. Customer Checkout: User verifies phone, adds payment, creates wallet
  2. Connection Created: Wallet is linked to your merchant account
  3. Forward Token Generated: You create token from connection + product secrets
  4. API Requests: Customer makes AI requests with forward token
  5. Usage Metered: Lava tracks tokens, calculates costs based on your product pricing
  6. Balance Deducted: Customer wallet charged immediately (prepaid model)
  7. Revenue Tracked: Your merchant earnings accumulate in real-time
  8. Payouts: Stripe transfers funds to your bank on configured schedule
Key Concepts: Transfers Ledger Every request generates transfer records:
  • Base Cost: AI provider’s charge (e.g., OpenAI)
  • Merchant Fee: Your configured markup
  • Service Charge: Lava’s 1.9% platform fee
Settlement Transfers are immediately “settled” if wallet has funds. If insufficient:
  • Transfer created as “under-settled”
  • When customer adds funds, oldest transfers settled first
  • Merchant earnings only count settled amounts
Balance Management
  • Active Balance: Current spendable amount
  • Autopay: Auto-top-up when balance falls below threshold
  • Connection Status: “ok” (funded), “limited” (low balance), “disabled” (blocked)
Lava adds less than 20ms of latency through edge computing and Redis caching. Streaming responses work seamlessly, with usage tracked after completion.

Support

Need help with monetization setup? We’re here to assist:
  • Documentation: www.lava.so/docs
  • Support: [email protected]
  • Dashboard: Check Monetize > Revenue for detailed analytics
  • Community: Join our Discord for peer support and updates