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

# Checkout

> Embed Lava's checkout flow for subscriptions and credit bundle purchases

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.

<Tip>
  **New to Lava?** Start with the [Charge Your First Customer](/get-started/quickstart-charge) quickstart for a step-by-step tutorial. This page is the detailed reference for checkout modes, integration patterns, and completion handling.
</Tip>

## Checkout Modes

Lava checkout supports two modes, each for a different stage of the customer lifecycle.

| Mode            | Purpose                                    | Creates Customer? | Requires Customer? |
| --------------- | ------------------------------------------ | ----------------- | ------------------ |
| `subscription`  | Subscribe to a recurring plan              | Yes               | No                 |
| `credit_bundle` | Buy a fixed credit pack (subscribers only) | No                | Yes                |

### 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](/monetize/plans#auto-top-up) or manual bundle purchase)
* Customer can cancel anytime

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

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

```typescript theme={null}
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: 'sc_your_plan_id',
  customer_id: existingCustomerId ?? undefined, // optional: reuse existing customer
});

// Return session.checkout_session_token to your frontend
```

The parameters vary by mode:

| Parameter          | subscription | credit\_bundle |
| ------------------ | :----------: | :------------: |
| `origin_url`       |   required   |    required    |
| `plan_id`          |   required   |        —       |
| `customer_id`      |   optional   |    required    |
| `credit_bundle_id` |       —      |    required    |

<Warning>
  **Checkout sessions expire after 60 minutes.** Create a new session for each checkout attempt. Never reuse tokens across multiple users.
</Warning>

***

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

```tsx theme={null}
'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: 'sc_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>
  );
}
```

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

***

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

<Tip>
  **Store the `customer_id`** in your database alongside your internal user ID. This lets you look up a customer directly without iterating through lists.
</Tip>

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.

### Backend Webhooks (Recommended)

Configure a webhook to receive `customer.created` events for reliable server-side processing. See the [Webhooks guide](/integration/webhooks) for full setup instructions.

```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"
  }
}
```

### Combined Pattern (Best Practice)

Use callbacks for instant UX, webhooks for backend finalization:

```typescript theme={null}
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
```

<Warning>
  **Always verify webhook signatures.** Without verification, malicious actors could send fake events to your endpoint. See [Webhook Signature Verification](/integration/webhooks#signature-verification).
</Warning>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Session expired error" icon="clock">
    **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.
  </Accordion>

  <Accordion title="Checkout not opening" icon="eye-slash">
    **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
  </Accordion>

  <Accordion title="Phone verification failing" icon="phone-slash">
    **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
  </Accordion>

  <Accordion title="Webhook not receiving events" icon="webhook">
    **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
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Forward Proxy" icon="route" href="/gateway/forward-proxy">
    Generate forward tokens to bill customers on each request
  </Card>

  <Card title="Webhooks" icon="webhook" href="/integration/webhooks">
    Receive notifications for customer and balance events
  </Card>
</CardGroup>
