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 four modes, each for a different stage of the customer lifecycle.
ModePurposeCreates Connection?Requires Connection?
subscriptionSubscribe to a recurring planYesNo
topupAdd credits to existing balanceNoYes
credit_bundleBuy a fixed credit pack (subscribers only)NoYes
onboardingOne-time account creation (legacy)YesNo

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 connection_id to use for billing
Billing cycle:
  • Balance resets monthly to the plan amount
  • Overages auto-charge to the customer’s card
  • Customer can cancel anytime
Use one subscription CTA in your app. For new customers, omit connection_id (checkout will collect identity). For returning customers, include connection_id to reuse their existing connection.

Topup Mode

Use when: An existing customer wants to add more credits to their balance. What happens:
  1. Customer verifies identity via SMS OTP (sent to the phone on file)
  2. Customer chooses an amount to add
  3. Customer is charged immediately
  4. Balance increases by that amount
Common use cases:
  • “Add Credits” button in your app
  • Low balance warning with recharge CTA
  • Letting power users pre-fund larger amounts
Topup requires an existing connection_id. The customer must have already completed checkout via subscription or onboarding mode first.

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 subscription configs (each config’s credit bundles include the ID).

Onboarding Mode (Legacy)

Use when: You want one-time account creation without recurring billing. What happens:
  1. Customer verifies phone number
  2. Customer adds payment method
  3. Customer adds an initial credit balance (one-time charge)
  4. You receive a connection_id
Key difference from subscription: no automatic renewal, no plan attached. Customer must manually top up when credits run low.
Legacy mode. For most use cases, subscription mode is preferred. It provides a better customer experience with automatic renewal and predictable billing.

Which Mode Should I Use?

Is this a new customer?
├── Yes → Do you want recurring monthly billing?
│   ├── Yes → subscription
│   └── No → onboarding (legacy)
└── No → Existing customer wants…
    ├── To add an arbitrary amount → topup
    └── To buy a fixed credit pack (subscriber only) → 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',
  subscription_config_id: 'sub_your_plan_id',
  connection_id: existingConnectionId ?? undefined, // optional: reuse existing connection
});

// Return session.checkout_session_token to your frontend
The parameters vary by mode:
Parametersubscriptiontopupcredit_bundleonboarding
origin_urlrequiredrequiredrequiredrequired
subscription_config_idrequired
connection_idoptionalrequiredrequired
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: ({ connectionId }) => {
      // Save connectionId to your database, linked to your user
      fetch('/api/user/save-connection', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ connectionId })
      });
      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',
          subscription_config_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 connection_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 connection_id in your database alongside your internal user ID. This lets you look up a customer’s connection directly without iterating through lists.
The connection object includes has_lava_credit_balance: boolean — use this to check whether a customer has available credits before making requests. 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 connection.created events for reliable server-side processing. See the Webhooks guide for full setup instructions.
{
  "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"
  }
}

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 connectionId 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