Documentation Index
Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
Use this file to discover all available pages before exploring further.
Table of Contents
Installation
Install the required packages:
npm install @solvapay/server express
# or
pnpm add @solvapay/server express
# or
yarn add @solvapay/server express
Basic Setup
1. Initialize SolvaPay
Create a SolvaPay instance in your Express app:
import express from 'express'
import { createSolvaPay } from '@solvapay/server'
const app = express()
app.use(express.json())
// Initialize SolvaPay
const solvaPay = createSolvaPay({
apiKey: process.env.SOLVAPAY_SECRET_KEY, // Optional: defaults to env var
})
// Create payable handler for your product
const payable = solvaPay.payable({
product: 'prd_YOUR_PRODUCT_ID',
})
2. Protect Your First Endpoint
Wrap your business logic with the payable.http() adapter:
// Your business logic function
async function createTask(req: express.Request) {
const { title, description } = req.body
// Your business logic here
const task = {
id: Date.now().toString(),
title,
description,
createdAt: new Date().toISOString(),
}
return { success: true, task }
}
// Protect the endpoint
app.post('/api/tasks', payable.http(createTask))
That’s it! The endpoint is now protected. The paywall will:
- Check if the customer has a valid purchase
- Track usage and enforce limits
- Return a paywall error with checkout URL if needed
Protecting Endpoints
Multiple Endpoints
Create one payable handler and apply it to all protected endpoints:
const payable = solvaPay.payable({
product: 'prd_myapi',
})
app.post('/api/tasks', payable.http(createTask))
app.get('/api/tasks/:id', payable.http(getTask))
app.delete('/api/tasks/:id', payable.http(deleteTask))
Plans are managed on the product in SolvaPay Console. Customers select a plan during activation and the SDK resolves the correct plan from their purchase automatically.
Accessing Request Data
The HTTP adapter passes the Express request object to your business logic:
async function createTask(req: express.Request) {
// Access request body
const { title } = req.body
// Access route parameters
const { id } = req.params
// Access query parameters
const { limit } = req.query
// Access headers
const authToken = req.headers.authorization
// Your business logic
return { success: true, task: { title } }
}
app.post('/api/tasks', payable.http(createTask))
Authentication Integration
SolvaPay needs to identify customers. You can pass customer references in several ways:
Extract customer reference from a custom header:
import { getUserIdFromRequest } from '@solvapay/auth'
// Custom middleware to extract customer reference
function extractCustomerRef(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
// Extract from header, JWT token, session, etc.
const customerRef = req.headers['x-customer-ref'] as string
if (!customerRef) {
return res.status(401).json({ error: 'Missing customer reference' })
}
// Attach to request for SolvaPay to use
req.customerRef = customerRef
next()
}
app.use(extractCustomerRef)
// SolvaPay will automatically use req.customerRef
app.post('/api/tasks', payable.http(createTask))
Option 2: JWT Token
Extract customer ID from a JWT token:
import jwt from 'jsonwebtoken'
import { getUserIdFromRequest } from '@solvapay/auth'
function authMiddleware(req: express.Request, res: express.Response, next: express.NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Missing token' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
req.customerRef = decoded.userId
next()
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
app.use(authMiddleware)
app.post('/api/tasks', payable.http(createTask))
Use the getCustomerRef option for more complex scenarios:
import jwt from 'jsonwebtoken'
// Extract customer reference from JWT token
app.post(
'/api/tasks',
payable.http(createTask, {
getCustomerRef: (req: express.Request) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return null
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
return decoded.userId
},
}),
)
Error Handling
Basic Error Handling
The HTTP adapter routes paywall gate outcomes through a formatGate
channel that emits a 402 response with the standard JSON body
({success:false, error, product, checkoutUrl, message, ...}). Merchants
don’t need to catch anything for the happy path:
async function createTask(req: express.Request) {
// Your business logic
return { success: true, task: {} }
}
// 402 paywall responses are emitted by the adapter automatically.
app.post('/api/tasks', payable.http(createTask))
Custom Paywall Responses
Use paywall.decide() directly when you want full control over the
402 response shape (e.g. a hand-rolled route that doesn’t use
payable.http()):
app.post('/api/tasks', async (req, res) => {
const decision = await solvaPay.paywall.decide(req.body, {
product: 'prd_myapi',
})
if (decision.outcome === 'gate') {
return res.status(402).json({
error: decision.gate.kind === 'activation_required'
? 'Activation required'
: 'Payment required',
product: decision.gate.product,
checkoutUrl: decision.gate.checkoutUrl,
message: decision.gate.message,
})
}
const task = await createTask(req)
res.json(task)
})
Legacy PaywallError Compat
Consumers that still try/catch a PaywallError keep working —
PaywallError is exported as a compat shim for merchant code that
throws from deep inside business logic:
import { PaywallError } from '@solvapay/server'
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (error instanceof PaywallError) {
return res.status(402).json({
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
...error.structuredContent,
})
}
console.error('Unhandled error:', error)
res.status(500).json({ error: 'Internal server error' })
})
Advanced Usage
Override customer reference extraction per endpoint:
app.post(
'/api/tasks',
payable.http(createTask, {
getCustomerRef: (req: express.Request) => {
// Custom logic to extract customer reference
return req.headers['x-customer-ref'] as string
},
}),
)
Transform responses before sending:
app.post(
'/api/tasks',
payable.http(createTask, {
transformResponse: (result, reply) => {
// Custom response formatting
reply.status(201).json({ data: result, timestamp: new Date().toISOString() })
},
}),
)
The HTTP adapter automatically formats responses. Your business logic should return:
- Object: Sent as JSON with 200 status
- Error: Thrown as exception (PaywallError handled automatically)
async function createTask(req: express.Request) {
// Return object - automatically sent as JSON
return { success: true, task: {} }
// Or throw error - automatically handled
if (!req.body.title) {
throw new Error('Title is required')
}
}
Complete Example
Here’s a complete Express.js application with SolvaPay integration:
import express, { type Express, type Request, type Response } from 'express'
import { createSolvaPay, PaywallError } from '@solvapay/server'
const app: Express = express()
const port = process.env.PORT || 3000
// Middleware
app.use(express.json())
// Initialize SolvaPay
const solvaPay = createSolvaPay({
apiKey: process.env.SOLVAPAY_SECRET_KEY,
})
// Create payable handlers
const payable = solvaPay.payable({
product: 'prd_myapi',
})
// Authentication middleware
function authMiddleware(req: Request, res: Response, next: express.NextFunction) {
const customerRef = req.headers['x-customer-ref'] as string
if (!customerRef) {
return res.status(401).json({ error: 'Missing x-customer-ref header' })
}
req.customerRef = customerRef
next()
}
app.use(authMiddleware)
// Business logic functions
async function createTask(req: Request) {
const { title, description } = req.body
if (!title) {
throw new Error('Title is required')
}
const task = {
id: Date.now().toString(),
title,
description,
createdAt: new Date().toISOString(),
}
return { success: true, task }
}
async function getTask(req: Request) {
const { id } = req.params
// Simulate fetching from database
const task = {
id,
title: 'Sample Task',
description: 'Task description',
}
return { success: true, task }
}
async function listTasks(req: Request) {
const tasks = [
{ id: '1', title: 'Task 1' },
{ id: '2', title: 'Task 2' },
]
return { success: true, tasks, total: tasks.length }
}
// Protected routes
app.post('/api/tasks', payable.http(createTask))
app.get('/api/tasks/:id', payable.http(getTask))
app.get('/api/tasks', payable.http(listTasks))
// Health check (unprotected)
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok' })
})
// Error handling middleware — the HTTP adapter already emits 402
// paywall responses via `formatGate`, so anything landing here is a
// genuine uncaught error. The `instanceof PaywallError` branch
// below is a compat shim for merchant code that still throws
// `PaywallError` directly from deep business logic.
app.use((error: Error, req: Request, res: Response, next: express.NextFunction) => {
if (error instanceof PaywallError) {
return res.status(402).json({
error: 'Payment required',
message: error.message,
checkoutUrl: error.structuredContent.checkoutUrl,
product: error.structuredContent.product,
})
}
console.error('Error:', error)
res.status(500).json({ error: 'Internal server error' })
})
// Start server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`)
})
Testing the Example
# Start the server
npm start
# Test with customer reference
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-H "x-customer-ref: user_123" \
-d '{"title": "My Task", "description": "Task description"}'
Best Practices
-
Extract Customer Reference Early: Use middleware to extract and validate customer references before they reach protected endpoints.
-
Handle Paywall Errors Gracefully: Provide clear error messages and checkout URLs to users.
-
Use Environment Variables: Store API keys and configuration in environment variables.
-
Separate Business Logic: Keep your business logic functions separate from route handlers for better testability.
-
Type Safety: Use TypeScript for better type safety and developer experience.
-
Error Logging: Log errors appropriately for debugging while keeping sensitive information secure.
Next Steps