> ## 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.

# Express.js Integration Guide

> This guide shows you how to integrate SolvaPay SDK with Express.js to protect your API endpoints with paywall protection and purchase management.

## Table of Contents

* [Installation](#installation)
* [Basic Setup](#basic-setup)
* [Protecting Endpoints](#protecting-endpoints)
* [Authentication Integration](#authentication-integration)
* [Error Handling](#error-handling)
* [Advanced Usage](#advanced-usage)
* [Complete Example](#complete-example)

## Installation

Install the required packages:

```bash theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
// 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:

```typescript theme={null}
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:

```typescript theme={null}
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:

### Option 1: Custom Header (Recommended)

Extract customer reference from a custom header:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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()`):

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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)

```typescript theme={null}
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:

```typescript theme={null}
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

```bash theme={null}
# 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

* [Next.js Integration Guide](./nextjs) - Learn how to integrate with Next.js
* [Webhooks](./webhooks) - Handle async purchase and subscription events
* [Custom Authentication Adapters](./custom-auth) - Build custom auth adapters
* [API Reference](/sdks/typescript/intro) - Full API documentation
