Overview
The Lava checkout component handles the complete customer onboarding flow: phone verification, wallet creation, payment processing, and connection establishment. This guide shows you how to integrate it into your React application with both frontend callbacks and backend webhooks.
Prerequisites: You need a Lava merchant account with a product configured. See the Monetize Quickstart to set up your first product.
Installation
Install the Checkout Package
npm install @lavapayments/checkout
Install the Node.js SDK (Backend)
npm install @lavapayments/nodejs
The checkout component (@lavapayments/checkout) is frontend-only. You’ll need the Node.js SDK (@lavapayments/nodejs) for creating checkout sessions on your backend.
Backend Session Creation
Before embedding the checkout component, create a checkout session on your backend. This ensures security and lets you configure checkout behavior.
Session Types
Lava supports three checkout modes:
Mode Purpose When to Use onboardingCreate wallet + add funds First-time user sign-up, no existing wallet topupAdd funds to existing wallet User needs more credits subscriptionSubscribe to recurring billing Monthly/annual billing plans
Next.js API Route Example
// app/api/checkout/create-session/route.ts
import { NextRequest , NextResponse } from 'next/server' ;
import { Lava } from '@lavapayments/nodejs' ;
const lava = new Lava ({
secretKey: process . env . LAVA_SECRET_KEY !
});
export async function POST ( req : NextRequest ) {
const { mode , amount , productId , userReferenceId } = await req . json ();
try {
const session = await lava . checkout . createSession ({
mode: mode , // 'onboarding', 'topup', or 'subscription'
productId: productId , // Your product secret
amount: amount , // In cents (e.g., 5000 = $50.00)
referenceId: userReferenceId , // Your internal user ID
metadata: {
userId: userReferenceId ,
plan: 'pro' ,
source: 'pricing_page'
}
});
return NextResponse . json ({
sessionSecret: session . sessionSecret ,
sessionId: session . sessionId
});
} catch ( error ) {
console . error ( 'Session creation failed:' , error );
return NextResponse . json (
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}
Express.js Example
import express from 'express' ;
import { Lava } from '@lavapayments/nodejs' ;
const app = express ();
app . use ( express . json ());
const lava = new Lava ({ secretKey: process . env . LAVA_SECRET_KEY ! });
app . post ( '/api/checkout/create-session' , async ( req , res ) => {
const { mode , amount , productId , userReferenceId } = req . body ;
try {
const session = await lava . checkout . createSession ({
mode: mode ,
productId: productId ,
amount: amount ,
referenceId: userReferenceId ,
metadata: {
userId: userReferenceId ,
plan: req . body . plan || 'basic' ,
source: 'api'
}
});
res . json ({
sessionSecret: session . sessionSecret ,
sessionId: session . sessionId
});
} catch ( error ) {
console . error ( 'Session creation error:' , error );
res . status ( 500 ). json ({ error: 'Failed to create checkout session' });
}
});
Session secrets expire after 30 minutes. Create a new session for each checkout attempt. Never reuse session secrets across multiple users.
Session Configuration Options
await lava . checkout . createSession ({
mode: 'subscription' ,
productId: 'prod_abc123' ,
amount: 5000 ,
// Optional: Link to your internal user
referenceId: 'user_xyz789' ,
// Optional: Attach custom data
metadata: {
userId: 'user_xyz789' ,
plan: 'pro' ,
annualBilling: true ,
promoCode: 'LAUNCH2024'
},
// Optional: Redirect URLs (for hosted checkout)
successUrl: 'https://yourapp.com/dashboard?checkout=success' ,
cancelUrl: 'https://yourapp.com/pricing?checkout=cancelled'
});
Frontend Component Integration
Basic Integration
'use client' ; // Next.js App Router
import { LavaCheckout } from '@lavapayments/checkout' ;
import { useState } from 'react' ;
export function CheckoutButton () {
const [ sessionSecret , setSessionSecret ] = useState < string | null >( null );
const [ loading , setLoading ] = useState ( false );
async function startCheckout () {
setLoading ( true );
try {
// Call your backend to create session
const response = await fetch ( '/api/checkout/create-session' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
mode: 'onboarding' ,
amount: 5000 , // $50.00
productId: process . env . NEXT_PUBLIC_LAVA_PRODUCT_ID ,
userReferenceId: 'user_123'
})
});
const { sessionSecret } = await response . json ();
setSessionSecret ( sessionSecret );
} catch ( error ) {
console . error ( 'Failed to create session:' , error );
alert ( 'Failed to start checkout. Please try again.' );
} finally {
setLoading ( false );
}
}
return (
< div >
{ ! sessionSecret ? (
< button onClick = { startCheckout } disabled = { loading } >
{ loading ? 'Loading...' : 'Get Started - $50 Credit' }
</ button >
) : (
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { ( connection ) => {
console . log ( 'Checkout completed!' , connection );
// Save connection details
saveConnection ( connection );
// Redirect to dashboard
window . location . href = '/dashboard?checkout=success' ;
} }
onError = { ( error ) => {
console . error ( 'Checkout error:' , error );
alert ( 'Checkout failed. Please try again.' );
setSessionSecret ( null ); // Allow retry
} }
/>
) }
</ div >
);
}
async function saveConnection ( connection : any ) {
// Store connection secret in your database
await fetch ( '/api/user/connection' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
connectionId: connection . connectionId ,
connectionSecret: connection . connectionSecret
})
});
}
With Loading and Error States
import { LavaCheckout } from '@lavapayments/checkout' ;
import { useState } from 'react' ;
export function CheckoutFlow () {
const [ sessionSecret , setSessionSecret ] = useState < string | null >( null );
const [ checkoutState , setCheckoutState ] = useState < 'idle' | 'loading' | 'checkout' | 'success' | 'error' >( 'idle' );
const [ errorMessage , setErrorMessage ] = useState < string | null >( null );
async function handleStartCheckout () {
setCheckoutState ( 'loading' );
setErrorMessage ( null );
try {
const response = await fetch ( '/api/checkout/create-session' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
mode: 'onboarding' ,
amount: 5000 ,
productId: process . env . NEXT_PUBLIC_LAVA_PRODUCT_ID
})
});
if ( ! response . ok ) {
throw new Error ( 'Failed to create session' );
}
const { sessionSecret } = await response . json ();
setSessionSecret ( sessionSecret );
setCheckoutState ( 'checkout' );
} catch ( error ) {
setCheckoutState ( 'error' );
setErrorMessage ( 'Failed to initialize checkout. Please try again.' );
}
}
if ( checkoutState === 'success' ) {
return (
< div className = "success-message" >
< h2 > 🎉 Welcome! </ h2 >
< p > Your wallet has been created. Redirecting to dashboard... </ p >
</ div >
);
}
if ( checkoutState === 'error' ) {
return (
< div className = "error-message" >
< p > { errorMessage } </ p >
< button onClick = { () => setCheckoutState ( 'idle' ) } > Try Again </ button >
</ div >
);
}
if ( checkoutState === 'loading' ) {
return < div > Preparing checkout... </ div > ;
}
if ( checkoutState === 'checkout' && sessionSecret ) {
return (
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { ( connection ) => {
setCheckoutState ( 'success' );
saveConnectionToBackend ( connection );
setTimeout (() => {
window . location . href = '/dashboard' ;
}, 2000 );
} }
onError = { ( error ) => {
setCheckoutState ( 'error' );
setErrorMessage ( error . message || 'Checkout failed' );
} }
/>
);
}
return (
< button onClick = { handleStartCheckout } >
Start Checkout
</ button >
);
}
Handling Checkout Completion
You have two options for handling checkout completion: frontend callbacks (faster, less reliable) or backend webhooks (recommended for production).
Option 1: Frontend Callback (Development/Testing)
The onSuccess callback receives connection details immediately:
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { ( connection ) => {
// Connection details available immediately
console . log ( 'Connection ID:' , connection . connectionId );
console . log ( 'Connection Secret:' , connection . connectionSecret );
console . log ( 'Wallet ID:' , connection . walletId );
// Save to your backend
await fetch ( '/api/user/save-connection' , {
method: 'POST' ,
body: JSON . stringify ({
userId: currentUser . id ,
connectionId: connection . connectionId ,
connectionSecret: connection . connectionSecret
})
});
// Generate forward token for immediate use
const forwardToken = lava . generateForwardToken ({
connection_secret: connection . connectionSecret ,
product_secret: productSecret
});
// Redirect to app
window . location . href = '/dashboard' ;
} }
/>
Pros:
✅ Immediate feedback to user
✅ No webhook setup required
✅ Simple for development
Cons:
❌ User can close browser before callback executes
❌ No server-side verification
❌ Not suitable for production
Option 2: Backend Webhook (Production Recommended)
Configure a webhook to receive checkout.completed events:
Step 1: Create webhook endpoint
// app/api/webhooks/lava/route.ts
import { NextRequest , NextResponse } from 'next/server' ;
import { Lava } from '@lavapayments/nodejs' ;
import crypto from 'crypto' ;
const lava = new Lava ({ secretKey: process . env . LAVA_SECRET_KEY ! });
export async function POST ( req : NextRequest ) {
const body = await req . text ();
const signature = req . headers . get ( 'x-lava-signature' );
// Verify webhook signature
const expectedSignature = crypto
. createHmac ( 'sha256' , process . env . LAVA_WEBHOOK_SECRET ! )
. update ( body )
. digest ( 'hex' );
if ( signature !== expectedSignature ) {
return NextResponse . json ({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON . parse ( body );
// Handle checkout.completed event
if ( event . type === 'checkout.completed' ) {
const { connection_id , reference_id , wallet_id , amount } = event . data ;
// Retrieve full connection details
const connection = await lava . connections . retrieve ( connection_id );
// Save to your database
await database . users . update ({
where: { id: reference_id },
data: {
lavaWalletId: wallet_id ,
lavaConnectionId: connection_id ,
lavaConnectionSecret: connection . connection_secret ,
onboardingCompleted: true ,
initialCreditAmount: amount
}
});
// Send welcome email
await sendWelcomeEmail ( reference_id );
return NextResponse . json ({ received: true });
}
return NextResponse . json ({ received: true });
}
Step 2: Register webhook in Lava dashboard
Navigate to Monetize > Webhooks
Click “Add Endpoint”
Enter your URL: https://yourapp.com/api/webhooks/lava
Select events: checkout.completed, balance.updated, connection.deleted
Copy the Webhook Secret and add to your environment variables
Always verify webhook signatures. Without verification, malicious actors could send fake checkout completion events to your endpoint.
Webhook + Callback Pattern (Best Practice)
Combine both for the best user experience:
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { ( connection ) => {
// Show success message immediately (UX)
setCheckoutState ( 'success' );
showSuccessToast ( 'Wallet created! Setting up your account...' );
// Redirect to dashboard (webhook will finalize in background)
setTimeout (() => {
window . location . href = '/dashboard?checkout=success' ;
}, 2000 );
} }
/>
Backend webhook:
if ( event . type === 'checkout.completed' ) {
// Webhook finalizes setup
await setupUserAccount ( event . data );
await sendWelcomeEmail ( event . data . reference_id );
return NextResponse . json ({ received: true });
}
This pattern provides immediate user feedback while ensuring reliable backend processing.
Phone Verification Flow
The checkout component includes automatic phone verification via SMS OTP:
How It Works
User enters phone number in checkout component
Lava sends 6-digit code via Twilio SMS
User enters code to verify ownership
Wallet created and linked to verified phone
Payment processed after successful verification
Verification States
The component handles all verification states automatically:
Waiting for code - User sees OTP input field
Verifying code - Loading state while validating
Code expired - Option to resend after 10 minutes
Invalid code - Error message with retry option
Verified - Proceeds to payment step
Phone numbers can only be linked to one wallet. If a user tries to create a wallet with an existing phone number, they’ll be prompted to log into their existing wallet instead.
Rate Limiting
Lava implements rate limiting to prevent abuse:
5 OTP attempts per phone number per hour
10 checkout sessions per IP address per hour
3 failed payment attempts per session
If rate limits are exceeded, users see a friendly error message with retry instructions.
Customization Options
Theme Customization
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { handleSuccess }
theme = { {
// Colors
primaryColor: '#4f000b' , // Primary brand color
backgroundColor: '#ffffff' , // Background color
textColor: '#1a1a1a' , // Text color
errorColor: '#dc2626' , // Error messages
// Typography
fontFamily: 'Inter, system-ui, sans-serif' ,
fontSize: '16px' ,
// Borders & Radius
borderRadius: '8px' ,
borderColor: '#e5e7eb' ,
// Spacing
padding: '24px' ,
// Dark mode support
darkMode: false
} }
/>
Custom CSS Classes
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { handleSuccess }
className = "max-w-md mx-auto shadow-lg"
inputClassName = "border-2 border-gray-300 focus:border-blue-500"
buttonClassName = "bg-blue-600 hover:bg-blue-700 text-white"
/>
Locale and Currency
< LavaCheckout
sessionSecret = { sessionSecret }
onSuccess = { handleSuccess }
locale = "en-US" // 'en-US', 'es-ES', 'fr-FR'
currency = "USD" // 'USD', 'EUR', 'GBP'
phoneRegion = "US" // Default country for phone input
/>
Troubleshooting
Cause: Checkout session was created more than 30 minutes agoSolution:
Sessions expire after 30 minutes for security
Create a new session when user clicks checkout button
Don’t pre-create sessions on page load
Prevention: // ✅ Create session on button click
< button onClick = { createAndStartCheckout } > Start Checkout </ button >
// ❌ Don't create session on page load
useEffect (() => {
createCheckoutSession (); // Will expire if user waits
}, []);
Checkout component not rendering
Cause: Missing session secret or invalid formatSolution:
Verify session secret is passed correctly
Check browser console for errors
Ensure @lavapayments/checkout is installed
Confirm component is in a client component ('use client' directive)
Debug checklist: console . log ( 'Session secret:' , sessionSecret ); // Should start with 'cs_'
console . log ( 'Session secret length:' , sessionSecret ?. length ); // Should be >20 chars
Phone verification failing
Cause: Invalid phone number format or OTP delivery issuesSolution:
Use E.164 format: +1234567890 (no spaces or dashes)
Check phone number is valid and can receive SMS
Verify Twilio integration is working (Lava dashboard)
Check for rate limiting (5 attempts per hour per phone)
Common mistakes:
❌ (555) 123-4567 (formatted)
❌ 555-123-4567 (missing country code)
✅ +15551234567 (E.164 format)
Webhook not receiving events
Cause: Incorrect webhook URL, firewall blocking, or signature verification failingSolution:
Test webhook URL is publicly accessible: Use webhook.site to verify
Check webhook logs in Lava dashboard for delivery attempts
Verify signature validation is correct (use HMAC SHA-256)
Ensure webhook endpoint returns 200 status code
Check firewall/CORS settings aren’t blocking requests
Local testing:
Use ngrok to expose local dev server:ngrok http 3000
# Use ngrok URL in webhook configuration: https://abc123.ngrok.io/api/webhooks/lava
Next Steps