Skip to main content

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))

Option 3: Custom Customer Reference Extraction

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

Custom Customer Reference Extraction

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
    },
  }),
)

Response Transformation

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() })
    },
  }),
)

Response Formatting

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

  1. Extract Customer Reference Early: Use middleware to extract and validate customer references before they reach protected endpoints.
  2. Handle Paywall Errors Gracefully: Provide clear error messages and checkout URLs to users.
  3. Use Environment Variables: Store API keys and configuration in environment variables.
  4. Separate Business Logic: Keep your business logic functions separate from route handlers for better testability.
  5. Type Safety: Use TypeScript for better type safety and developer experience.
  6. Error Logging: Log errors appropriately for debugging while keeping sensitive information secure.

Next Steps