Skip to main content
Lava checkout is a hosted payment flow you embed in your app. It handles phone verification, payment setup, and subscription creation in a single full-screen iframe overlay.
New to Lava? Start with the Charge Your First Customer quickstart for a step-by-step tutorial. This page is the detailed reference for checkout modes, integration patterns, and completion handling.

Checkout Modes

Lava checkout supports two modes, each for a different stage of the customer lifecycle.
ModePurposeCreates Customer?Requires Customer?
subscriptionSubscribe to a recurring planYesNo
credit_bundleBuy a fixed credit pack (subscribers only)NoYes

Subscription Mode

Use when: A customer is starting or updating a recurring plan. This is the recommended mode for most use cases. What happens:
  1. Customer verifies phone number via SMS OTP (new customers enter their number; returning customers verify the phone on file)
  2. Customer adds payment method
  3. Customer is charged the plan amount (e.g., $25)
  4. Subscription is created with automatic monthly renewal
  5. You receive a customer_id to use for billing
Billing cycle:
  • Balance resets each cycle to the plan’s included credit
  • When credits run out, requests are blocked until credits are replenished (via auto top-up or manual bundle purchase)
  • Customer can cancel anytime
Use one subscription CTA in your app. For new customers, omit customer_id (checkout will collect identity). For returning customers, include customer_id to reuse their existing customer record.

Credit Bundle Mode

Use when: An existing subscriber wants to buy a fixed credit pack attached to their plan. What happens:
  1. Customer verifies identity via SMS OTP (sent to the phone on file)
  2. Customer sees the bundle (name, price, credit amount) and confirms payment
  3. Credits are added to their current subscription cycle
Bundle IDs are available in the dashboard when you manage plans (each plan’s credit bundles include the ID).

Which Mode Should I Use?

Is this a new customer?
├── Yes → subscription
└── No → Existing subscriber wants to buy a credit pack → credit_bundle

Backend: Create a Session

Before opening checkout, create a checkout session on your backend. The session token authenticates the checkout flow. origin_url must match the domain that opens the checkout iframe — Lava uses it to restrict which origins can embed the flow.
import { Lava } from '@lavapayments/nodejs';

const lava = new Lava();

const session = await lava.checkoutSessions.create({
  checkout_mode: 'subscription',
  origin_url: 'https://yourapp.com',
  plan_id: 'sub_your_plan_id',
  customer_id: existingCustomerId ?? undefined, // optional: reuse existing customer
});

// Return session.checkout_session_token to your frontend
The parameters vary by mode:
Parametersubscriptioncredit_bundle
origin_urlrequiredrequired
plan_idrequired
customer_idoptionalrequired
credit_bundle_idrequired
Checkout sessions expire after 60 minutes. Create a new session for each checkout attempt. Never reuse tokens across multiple users.

Frontend: Embed Checkout

The @lavapayments/checkout package exports a useLavaCheckout hook that opens a full-screen checkout iframe when you call open() with a session token.
'use client'; // Next.js App Router

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

export function SubscribeButton() {
  const [loading, setLoading] = useState(false);

  const { open } = useLavaCheckout({
    onSuccess: ({ customerId }) => {
      // Save customerId to your database, linked to your user
      fetch('/api/user/save-customer', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customerId })
      });
      window.location.href = '/dashboard?checkout=success';
    },
    onCancel: () => {
      console.log('Checkout cancelled by user');
    },
    onError: ({ error }) => {
      console.error('Checkout error:', error);
    }
  });

  async function startCheckout() {
    setLoading(true);
    try {
      const res = await fetch('/api/checkout/create-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          checkout_mode: 'subscription',
          plan_id: 'sub_your_plan_id'
        })
      });
      const { checkoutSessionToken } = await res.json();
      open(checkoutSessionToken);
    } catch (error) {
      console.error('Failed to create session:', error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <button onClick={startCheckout} disabled={loading}>
      {loading ? 'Loading...' : 'Subscribe Now'}
    </button>
  );
}
open() renders a full-screen iframe overlay. The checkout flow happens inside the iframe. When the user completes or cancels, Lava posts a message back to your page, triggering the appropriate callback.

Handling Completion

When checkout completes, you receive a customer_id — the billing relationship between this customer and your merchant account. This is the ID you’ll use for all future billing operations: generating forward tokens, checking balance, and retrieving usage.
Store the customer_id in your database alongside your internal user ID. This lets you look up a customer directly without iterating through lists.
Use frontend callbacks for immediate UX and backend webhooks for reliable processing. In production, use both.

Frontend Callbacks

The onSuccess callback fires immediately when checkout completes — use it for instant UI feedback like toasts and redirects. Pros: Immediate feedback, no setup required, great for development. Cons: User can close browser before callback executes. Not reliable enough for production alone. Configure a webhook to receive customer.created events for reliable server-side processing. See the Webhooks guide for full setup instructions.
{
  "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"
  }
}

Combined Pattern (Best Practice)

Use callbacks for instant UX, webhooks for backend finalization:
const { open } = useLavaCheckout({
  onSuccess: () => {
    showSuccessToast('Subscription created! Setting up your account...');
    setTimeout(() => {
      window.location.href = '/dashboard?checkout=success';
    }, 2000);
  }
});
// Webhook handler saves customerId to your database in the background
Always verify webhook signatures. Without verification, malicious actors could send fake events to your endpoint. See Webhook Signature Verification.

Troubleshooting

Cause: Checkout session was created more than 60 minutes ago.Solution: Create a new session when the user clicks the checkout button. Don’t pre-create sessions on page load.
Cause: Invalid or missing checkout session token.Solution:
  1. Verify @lavapayments/checkout is installed
  2. Ensure the component uses the 'use client' directive (Next.js App Router)
  3. Check that open() is called with the checkout_session_token from your backend
  4. Check browser console for errors
Cause: Invalid phone number format or OTP delivery issues.Solution:
  • Phone numbers must be in E.164 format: +15551234567
  • Verify the phone number can receive SMS
  • Common mistakes: (555) 123-4567 (formatted) and 555-123-4567 (missing country code) will not work
Cause: Incorrect webhook URL, firewall blocking, or signature verification failing.Solution:
  1. Verify webhook URL is publicly accessible
  2. Check webhook logs in the Lava dashboard
  3. Verify signature validation uses X-Webhook-Signature header with HMAC SHA-256
  4. Ensure your endpoint returns a 200 status code

Next Steps