React + Python Setup
Set up Lumen with React frontend and Python backend (Flask, FastAPI, or Django): connect Stripe, install packages, and implement billing.
1. Connect Stripe & Create Lumen Account
-
Stripe Keys: Get your test mode Secret and Publishable keys from Stripe Dashboard.
Using Dodo Payments?
Dodo Payments works similarly to Stripe but uses a single API Key (Secret Key) instead of two.
Just like Stripe, Dodo has Test Mode and Live Mode keys. Make sure to use your Test Key for this guide.
-
Lumen Account: Sign up at getlumen.dev.
Tip
We recommend creating two separate Lumen accounts (e.g. yourname+test@gmail.com and yourname@gmail.com) to keep test and live data completely separate. Alternatively, you can swap your test keys for live keys later.
- Onboarding: Enter your Stripe test keys and select USD as the currency.
2. Install Packages
Backend (Python):
# Install Flask with async support
pip install flask[async]
# Install Lumen SDK with Flask support
pip install lumen-python-sdk[flask]Important
The Lumen SDK uses async functions, so you need flask[async] which includes async support for Flask 3.0+.
# Install Lumen SDK with FastAPI support
pip install lumen-python-sdk[fastapi]FastAPI is async-native. The Lumen SDK will work seamlessly with async routes.
# Install Lumen SDK with Django support
pip install lumen-python-sdk[django]The Lumen SDK works with Django 3.1+ (which has built-in async support).
Requirements: Python >= 3.8
Frontend (React):
First, install the React SDK:
npm install @getlumen/reactThen, install the UI components. Choose the method that works for your setup:
Recommended method - Uses shadcn CLI to install components
npx shadcn@latest add https://getlumen.dev/pricing-table.json
npx shadcn@latest add https://getlumen.dev/usage-badge.jsonThis method works even if you don't have shadcn/ui set up in your project. The shadcn CLI will:
- Download the PricingTable and UsageBadge components
- Automatically configure your
components.jsonif it doesn't exist - Set up the necessary file structure
Requirements
Requires React and Tailwind CSS installed in your project.
Alternative method - Direct component installation
npx lumen addThis interactive command will:
- Download the PricingTable component (TypeScript or JavaScript)
- Configure Tailwind CSS automatically
- Add required CSS variables
For non-interactive installation:
# TypeScript
npx lumen add -l tsx -p src/components/pricing-table.tsx -y
# JavaScript
npx lumen add -l jsx -p src/components/pricing-table.jsx -yRequirements
This method requires Tailwind CSS installed in your project.
3. Environment Variables
During onboarding, you should have gotten Lumen API keys.
Backend (.env):
# you can get new keys at https://getlumen.dev/developer/apikeys if you lost them
LUMEN_API_KEY=lumen_sk_...
LUMEN_API_URL=https://api.getlumen.dev # Optional, defaults to productionFrontend (.env):
VITE_LUMEN_PUBLISHABLE_KEY=pk_live_...4. Lumen Handler (Backend)
Create a handler route to securely proxy Lumen requests (Frontend → Backend → Lumen).
Important
Make sure to update the code to use your auth middleware and get the user id from the request.
from flask import Flask, request
from lumen.handlers import lumen_flask_handler
app = Flask(__name__)
# Your auth function
def get_user_id():
# Example: Extract from JWT or session
user_id = request.headers.get("X-User-ID")
if not user_id:
return None
return user_id
# Lumen proxy route
@app.route("/api/lumen/<path:path>", methods=["GET", "POST", "PUT", "DELETE"])
def lumen_proxy(path):
handler = lumen_flask_handler(get_user_id=get_user_id)
return handler(path)
if __name__ == "__main__":
app.run(port=5000)from fastapi import FastAPI, Request, HTTPException
from lumen.handlers import lumen_fastapi_handler
app = FastAPI()
# Your auth function
def get_user_id(request: Request):
# Example: Extract from JWT or session
user_id = getattr(request.state, "user_id", None)
if not user_id:
raise HTTPException(status_code=401, detail="Unauthorized")
return user_id
# Create handler
handler = lumen_fastapi_handler(get_user_id=get_user_id)
# Lumen proxy route
@app.api_route("/api/lumen/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def lumen_proxy(request: Request, path: str):
return await handler(request, path)# views.py
from lumen.handlers import lumen_django_handler
def get_user_id(request):
if not request.user.is_authenticated:
return None
return str(request.user.id)
lumen_view = lumen_django_handler(get_user_id=get_user_id)
# urls.py
from django.urls import path
from .views import lumen_view
urlpatterns = [
path('api/lumen/<path:path>', lumen_view),
]5. Setup LumenProvider (Frontend)
To use Lumen's hooks and components, wrap your React app with the LumenProvider.
// src/main.tsx or src/App.tsx
import { LumenProvider } from "@getlumen/react";
function App() {
return (
<LumenProvider config={{ apiProxyUrl: "http://localhost:5000/api/lumen" }}>
{/* Your app components */}
</LumenProvider>
);
}
export default App;Production
In production, replace http://localhost:5000 with your actual backend URL. FastAPI typically runs on port 8000.
6. Webhooks (Recommended)
Lumen works best if every user has a subscription, even if it's just a free plan. This ensures that:
- Entitlements work immediately: You can check feature access for all users.
- Smooth Upgrades: Users can upgrade from Free → Pro without data migration issues.
To achieve this, we listen to user.created events from your auth provider to automatically create a free customer in Lumen.
-
Go to Configure > Webhooks in Clerk and create a webhook for
user.created. -
URL:
https://api.getlumen.dev/v1/webhooks/clerk
-
In the Advanced tab, add this header:
- Key:
Authorization - Value:
Bearer your_lumen_secret_key(from API Keys)

- Key:
-
Go to Webhooks > Create a new hook.
-
Trigger on INSERT in
auth.users. -
URL:
https://api.getlumen.dev/v1/webhooks/supabase -
Add this HTTP Header:
Authorization: Bearer your_lumen_secret_key

Install and configure the Lumen plugin in your frontend project:
npm install @getlumen/better-authimport { LumenPlugin } from "@getlumen/better-auth";
export const auth = betterAuth({
plugins: [ LumenPlugin({ apiKey: process.env.LUMEN_API_KEY! }) ],
});The plugin automatically enrolls new users in Lumen when they sign up.
For any other auth system, call enroll_user in your signup endpoint after creating the user in your database:
from lumen import enroll_user
from flask import request
@app.route('/api/auth/signup', methods=['POST'])
async def signup():
data = request.get_json()
email = data['email']
name = data['name']
password = data['password']
# 1. Create user in your auth system/database
user = create_user(email=email, name=name, password=password)
# 2. Enroll user in Lumen (creates free customer with subscription)
await enroll_user(
email=user.email,
name=user.name,
user_id=user.id, # Must match the ID from your auth system
)
return {'success': True, 'userId': user.id}from lumen import enroll_user
from pydantic import BaseModel
class SignupRequest(BaseModel):
email: str
name: str
password: str
@app.post('/api/auth/signup')
async def signup(request: SignupRequest):
# 1. Create user in your auth system/database
user = await create_user(
email=request.email,
name=request.name,
password=request.password
)
# 2. Enroll user in Lumen (creates free customer with subscription)
await enroll_user(
email=user.email,
name=user.name,
user_id=user.id, # Must match the ID from your auth system
)
return {'success': True, 'userId': user.id}from lumen import enroll_user
import json
from django.http import JsonResponse
async def signup_view(request):
data = json.loads(request.body)
email = data['email']
name = data['name']
password = data['password']
# 1. Create user in your auth system/database
user = create_user(email=email, name=name, password=password)
# 2. Enroll user in Lumen (creates free customer with subscription)
await enroll_user(
email=user.email,
name=user.name,
user_id=user.id, # Must match the ID from your auth system
)
return JsonResponse({'success': True, 'userId': user.id})Important: The user_id passed to Lumen must match the user ID from your authentication system, as this is how Lumen links subscriptions to users.
7. Create a Plan
In the Lumen Dashboard, go to Plans → Create. Let's create a test plan:
- Basic Info: Name it "Premium Plan".
- Pricing: Set Monthly Price to $10. Toggle on yearly price and set it to $100.
- Features: Click
Add New Featureto add entitlements:- Boolean Feature: Name it "Access to live customer service".
- Usage Feature: Name it "API Calls". Enable credit allowance of 500 (Monthly renewal) and set overage price to $0.05.
See screenshots of plan configuration


8. Add Pricing Table (Frontend)
Create a pricing page and drop in the PricingTable component. This component handles the full checkout flow automatically.
Import path depends on your installation method:
- Lumen CLI: Import from the path where you installed it (e.g.,
./components/pricing-table) - shadcn: Import from
@/components/ui/pricing-table
// src/pages/Pricing.tsx
import { PricingTable } from "./components/pricing-table"; // or "@/components/ui/pricing-table" if using shadcn
import { useAuth } from "./your-auth-provider"; // Your auth hook
export default function PricingPage() {
const { userId } = useAuth();
return (
<PricingTable
lumenPublishableKey={import.meta.env.VITE_LUMEN_PUBLISHABLE_KEY} // Adjust based on your bundler
userId={userId}
loginRedirectUrl="/login"
/>
);
}9. Entitlements: Frontend vs Backend
Lumen allows you to check feature access in two places. It's important to understand where and why to use each.
| Context | Purpose | Security |
|---|---|---|
| Frontend | UX/UI: Hide buttons, show upgrade prompts, display usage bars. | Not secure (users can bypass with DevTools). |
| Backend | Enforcement: Prevent API access, block database writes, track reliable usage. | Secure. This is the source of truth. |
Note: You rarely need to check "Does user have a subscription?". Instead, check "Does user have Feature X?". This decouples your code from plan names.
Frontend (UX)
Use these to improve user experience. They don't replace backend checks.
Hide/Gate Content
import { useFeature } from "@getlumen/react";
const { entitled } = useFeature(userId, "access-to-live-customer-service");
if (!entitled) return null;
return <LiveChatWidget />;import { FeatureGate, Paywall } from "@getlumen/react";
// Simple: Render null if not entitled
<FeatureGate
userId={userId}
feature="access-to-live-customer-service"
>
<LiveChatWidget />
</FeatureGate>
// Upgrade Prompt: Show a paywall if not entitled
<Paywall
userId={userId}
feature="access-to-live-customer-service"
onUpgrade={() => navigate('/pricing')}
>
<LiveChatWidget />
</Paywall>Show Usage
import { useUsageQuota } from "@getlumen/react";
const { used, limit, percentage } = useUsageQuota(userId, "api-calls");<UsageBadge
featureSlug="api-calls"
label="API Calls"
/>Backend (Security)
This is where you enforce limits. Always check entitlements here before performing sensitive actions.
from lumen import send_event, is_feature_entitled
@app.route('/api/expensive-operation', methods=['POST'])
async def expensive_operation():
user_id = get_user_id()
# 1. SECURITY CHECK: Does the user have the feature?
if not await is_feature_entitled(feature="api-calls", user_id=user_id):
return {'error': 'Upgrade required'}, 403
# 2. Execute Business Logic
result = do_expensive_work()
# 3. Track Usage
await send_event(name="api-calls", user_id=user_id)
return {'success': True, 'result': result}from lumen import send_event, is_feature_entitled
@app.post('/api/expensive-operation')
async def expensive_operation(request: Request):
user_id = get_user_id(request)
# 1. SECURITY CHECK: Does the user have the feature?
if not await is_feature_entitled(feature="api-calls", user_id=user_id):
raise HTTPException(status_code=403, detail="Upgrade required")
# 2. Execute Business Logic
result = await do_expensive_work()
# 3. Track Usage
await send_event(name="api-calls", user_id=user_id)
return {'success': True, 'result': result}from lumen import send_event, is_feature_entitled
from django.http import JsonResponse
async def expensive_operation(request):
user_id = str(request.user.id)
# 1. SECURITY CHECK: Does the user have the feature?
if not await is_feature_entitled(feature="api-calls", user_id=user_id):
return JsonResponse({'error': 'Upgrade required'}, status=403)
# 2. Execute Business Logic
result = do_expensive_work()
# 3. Track Usage
await send_event(name="api-calls", user_id=user_id)
return JsonResponse({'success': True, 'result': result})10. Test Checkout
Run both your frontend and backend, then navigate to your pricing page. Use these test cards to complete a purchase:
- Card:
4242 4242 4242 4242 - Expiry: Any future date (e.g. 12/34)
- CVC: Any 3 digits (e.g. 123)
After checkout, verify the subscription:
- Lumen Dashboard: Subscriptions page
- Stripe Dashboard: Stripe Payments (Test Mode)
- Card:
4242 4242 4242 4242 - Expiry:
06/32(must use this specific date) - CVC:
123(must use this specific code)
After checkout, verify the subscription:
- Lumen Dashboard: Subscriptions page
- Dodo Dashboard: Dodo Payments Dashboard
Going to Production
You are currently using Test Mode keys. When you are ready to launch:
- Create a new Lumen Account (or use a separate team/workspace if available) to keep production data clean.
- Get your Live Mode keys from Stripe.
- Update your environment variables with the new Live keys.
Done!
Lumen billing is now fully integrated. You can test upgrades, downgrades, and cancellations.
Additional Python SDK Features
The Python SDK provides additional utilities for advanced use cases:
Seat Management (Team/Organization Plans)
from lumen import add_seat, remove_seat, get_seat_usage
# Add user to organization
await add_seat(
new_user_id="user_456",
organisation_user_id="org_owner_123",
metadata={"role": "developer"}
)
# Remove user
await remove_seat(
removed_user_id="user_456",
organisation_user_id="org_owner_123"
)
# Check seat usage
usage = await get_seat_usage(customer_id="cust_123")Subscription Status
from lumen import get_subscription_status, get_customer_overview
# Check active subscription
status = await get_subscription_status(user_id="user_123")
# Returns: {"hasActiveSubscription": True, "customer": {...}}
# Retrieve customer details
overview = await get_customer_overview(user_id="user_123")Event Tracking with Idempotency
from lumen import send_event
# Track with idempotency key to prevent duplicates
await send_event(
name="api_call",
value=1,
user_id="user_123",
idempotency_key="unique_key_123"
)