Payment Gates
Payment gates allow you to control access to specific resources or operations based on user subscription status. UIGen provides a flexible system for marking resources and operations as monetized, with backend enforcement and automatic upgrade prompts.
Overview
Payment gates work through a simple flow:
- Mark resources/operations as monetized in your OpenAPI spec
- Backend enforces limits and returns 402 Payment Required when exceeded
- Frontend intercepts 402 and shows upgrade prompt
- User upgrades via auto-generated pricing page
Resource-Level Gates
Mark an entire resource as monetized to require payment for all operations:
paths:
/api/v1/meetings:
x-uigen-monetized: true
get:
summary: List meetings
# Requires payment
post:
summary: Create meeting
# Requires payment
/api/v1/meetings/{id}:
x-uigen-monetized: true
get:
summary: Get meeting
# Requires payment
put:
summary: Update meeting
# Requires payment
delete:
summary: Delete meeting
# Requires payment
Result:
- All meeting operations require payment
- Backend enforces limits on all operations
- 402 responses trigger upgrade prompt
Operation-Level Gates
Mark specific operations as monetized for fine-grained control:
paths:
/api/v1/templates:
get:
summary: List templates
# Free for all users
post:
summary: Create template
x-uigen-monetized: true
# Requires payment
/api/v1/templates/{id}:
get:
summary: Get template
# Free for all users
put:
summary: Update template
x-uigen-monetized: true
# Requires payment
delete:
summary: Delete template
x-uigen-monetized: true
# Requires payment
Result:
- Users can list and view templates for free
- Creating, updating, and deleting templates requires payment
- Backend enforces limits only on monetized operations
Custom Messages
Provide custom upgrade messages for better user experience:
paths:
/api/v1/meetings:
x-uigen-monetized:
monetized: true
message: "Upgrade to Professional to create unlimited meetings"
post:
summary: Create meeting
Result:
- When backend returns 402, frontend shows custom message
- Message appears in upgrade prompt
- Helps users understand why they need to upgrade
Custom Redirects
Redirect users to custom URLs instead of the default pricing page:
paths:
/api/v1/enterprise-features:
x-uigen-monetized:
monetized: true
message: "This feature is only available on Enterprise plans"
redirectTo: "/contact-sales"
post:
summary: Use enterprise feature
Result:
- When backend returns 402, frontend redirects to
/contact-sales - Useful for custom pricing, enterprise sales, etc.
Backend Enforcement
The backend is the source of truth for payment enforcement. Here's how to implement it:
FastAPI Example
from fastapi import APIRouter, HTTPException, Depends
from app.auth import get_current_user
from app.models import User
router = APIRouter()
@router.post("/api/v1/meetings")
async def create_meeting(
meeting: MeetingCreate,
user: User = Depends(get_current_user)
):
# Check user's plan and limits
if user.plan == "free":
meeting_count = await get_user_meeting_count(user.id)
if meeting_count >= 10:
raise HTTPException(
status_code=402,
detail="Upgrade to Professional to create more meetings"
)
# Create meeting
return await create_meeting(user, meeting)
@router.post("/api/v1/templates")
async def create_template(
template: TemplateCreate,
user: User = Depends(get_current_user)
):
# Only paid users can create templates
if user.plan == "free":
raise HTTPException(
status_code=402,
detail="Upgrade to Professional to create custom templates"
)
# Create template
return await create_template(user, template)
Express.js Example
const express = require('express');
const router = express.Router();
router.post('/api/v1/meetings', async (req, res) => {
const user = req.user;
// Check user's plan and limits
if (user.plan === 'free') {
const meetingCount = await getUserMeetingCount(user.id);
if (meetingCount >= 10) {
return res.status(402).json({
error: 'Upgrade to Professional to create more meetings'
});
}
}
// Create meeting
const meeting = await createMeeting(user, req.body);
res.json(meeting);
});
router.post('/api/v1/templates', async (req, res) => {
const user = req.user;
// Only paid users can create templates
if (user.plan === 'free') {
return res.status(402).json({
error: 'Upgrade to Professional to create custom templates'
});
}
// Create template
const template = await createTemplate(user, req.body);
res.json(template);
});
Frontend Behavior
UIGen automatically handles 402 responses:
Upgrade Prompt
When backend returns 402, UIGen shows an upgrade prompt:
┌─────────────────────────────────────┐
│ 🔒 │
│ Upgrade Required │
│ │
│ Upgrade to Professional to create │
│ unlimited meetings │
│ │
│ [ View Plans ] │
└─────────────────────────────────────┘
Inline Mode
For operation-level gates, upgrade prompt appears inline:
┌─────────────────────────────────────┐
│ Create Meeting │
│ │
│ ┌───────────────────────────────┐ │
│ │ 🔒 Upgrade Required │ │
│ │ Upgrade to Professional to │ │
│ │ create unlimited meetings │ │
│ │ [ View Plans ] │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Full-Page Mode
For resource-level gates, upgrade prompt appears full-page:
┌─────────────────────────────────────┐
│ │
│ 🔒 │
│ Upgrade Required │
│ │
│ Upgrade to Professional to access │
│ meetings │
│ │
│ [ View Plans ] │
│ │
└─────────────────────────────────────┘
Best Practices
1. Use Resource-Level Gates for Core Features
# Good - Entire resource requires payment
paths:
/api/v1/meetings:
x-uigen-monetized: true
2. Use Operation-Level Gates for Freemium
# Good - Read is free, write requires payment
paths:
/api/v1/templates:
get:
summary: List templates
# Free
post:
summary: Create template
x-uigen-monetized: true
# Paid
3. Provide Clear Messages
# Good - Clear, actionable message
x-uigen-monetized:
monetized: true
message: "Upgrade to Professional to create unlimited meetings"
# Bad - Vague message
x-uigen-monetized:
monetized: true
message: "Payment required"
4. Backend Enforces, Frontend Reacts
# Good - Backend enforces limits
if user.plan == "free" and meeting_count >= 10:
raise HTTPException(status_code=402)
# Bad - Frontend-only checks (can be bypassed)
# Don't rely on frontend checks alone
5. Use Custom Redirects for Special Cases
# Good - Enterprise features redirect to sales
x-uigen-monetized:
monetized: true
redirectTo: "/contact-sales"
# Good - Custom pricing redirects to custom page
x-uigen-monetized:
monetized: true
redirectTo: "/custom-pricing"
Advanced Patterns
Plan Hierarchy
Check for specific plans in backend:
PLAN_HIERARCHY = {
"free": 0,
"pro": 1,
"enterprise": 2
}
def has_access(user_plan: str, required_plan: str) -> bool:
return PLAN_HIERARCHY.get(user_plan, 0) >= PLAN_HIERARCHY.get(required_plan, 0)
@router.post("/api/v1/advanced-features")
async def use_advanced_feature(user: User = Depends(get_current_user)):
if not has_access(user.plan, "enterprise"):
raise HTTPException(
status_code=402,
detail="This feature requires an Enterprise plan"
)
# Use feature
Usage-Based Limits
Track usage and enforce limits:
@router.post("/api/v1/ai-generations")
async def generate_ai_content(user: User = Depends(get_current_user)):
# Get user's monthly usage
usage = await get_monthly_usage(user.id)
# Check limits based on plan
limits = {
"free": 10,
"pro": 100,
"enterprise": float('inf')
}
if usage >= limits.get(user.plan, 0):
raise HTTPException(
status_code=402,
detail=f"Upgrade to increase your monthly limit from {limits[user.plan]} generations"
)
# Generate content
result = await generate_content(user, request)
await increment_usage(user.id)
return result
Feature Flags
Combine with feature flags for gradual rollout:
@router.post("/api/v1/beta-features")
async def use_beta_feature(user: User = Depends(get_current_user)):
# Check if user has access to beta
if not user.has_beta_access:
raise HTTPException(
status_code=402,
detail="Beta features are only available to Pro and Enterprise users"
)
# Use beta feature
Testing
Test 402 Responses
# Test that free users get 402
def test_free_user_limit():
# Create free user with 10 meetings
user = create_free_user()
create_meetings(user, count=10)
# Attempt to create 11th meeting
response = client.post("/api/v1/meetings", auth=user)
assert response.status_code == 402
assert "Upgrade" in response.json()["detail"]
# Test that paid users don't get 402
def test_paid_user_no_limit():
# Create pro user with 100 meetings
user = create_pro_user()
create_meetings(user, count=100)
# Attempt to create 101st meeting
response = client.post("/api/v1/meetings", auth=user)
assert response.status_code == 201
Test Upgrade Flow
// Test that 402 shows upgrade prompt
test('shows upgrade prompt on 402', async () => {
// Mock API to return 402
mockApi.post('/api/v1/meetings').reply(402, {
detail: 'Upgrade to Professional'
});
// Attempt to create meeting
render(<CreateMeetingForm />);
await userEvent.click(screen.getByText('Create'));
// Verify upgrade prompt shown
expect(screen.getByText('Upgrade Required')).toBeInTheDocument();
expect(screen.getByText('View Plans')).toBeInTheDocument();
});
Troubleshooting
Issue: Upgrade prompt not showing
Check:
- Is backend returning 402 status code?
- Is
x-uigen-monetizedannotation present? - Is MonetizationHandler wrapping the component?
Issue: Custom message not displaying
Check:
- Is message field set in
x-uigen-monetized? - Is backend returning the message in 402 response?
- Is message being passed to UpgradePrompt component?
Issue: Redirect not working
Check:
- Is
redirectTofield set inx-uigen-monetized? - Is the redirect URL valid?
- Is navigation working correctly?
Summary
Payment gates provide a flexible way to monetize your application:
- Resource-level gates - Entire resource requires payment
- Operation-level gates - Specific operations require payment
- Custom messages - Clear upgrade prompts
- Custom redirects - Flexible upgrade flows
- Backend enforcement - Secure, cannot be bypassed
- Automatic UI - No frontend code needed
The backend is always the source of truth, ensuring secure payment enforcement while providing a seamless user experience.