Node.js SDK

Beta

The Orvion Node.js SDK provides seamless integration with Express for creating payment-protected API endpoints using the x402 protocol.

Quick Start

1. Install the SDK

npm install @orvion/sdk

2. Initialize Orvion

TypeScript
import express from 'express'
import { orvionInit } from '@orvion/sdk/express'
const app = express()
// Initialize Orvion
orvionInit({
apiKey: process.env.ORVION_API_KEY!
})

3. Protect Endpoints

TypeScript
import { requirePayment } from '@orvion/sdk/express'
// Define a protected endpoint with pricing
app.get('/api/premium/data',
requirePayment({ amount: '0.10', currency: 'USDC' }),
(req, res) => {
res.json({
data: 'Premium content!',
paid: req.payment.amount
})
}
)

Pre-built Payment Router (NEW in v0.2)

Eliminate boilerplate by using the pre-built payment router:

TypeScript
import { OrvionClient } from '@orvion/sdk'
import { createPaymentRouter } from '@orvion/sdk/express'
const client = new OrvionClient({ apiKey: process.env.ORVION_API_KEY! })
// Add all payment endpoints with one line!
app.use('/api/payments', createPaymentRouter(client))
// This adds:
// POST /api/payments/confirm - Confirm wallet payment
// POST /api/payments/cancel/:id - Cancel pending charge
// GET /api/payments/state/:id - Get charge state for UI
// GET /api/payments/charge/:id - Get full charge details

orvionInit

Initialize the Orvion SDK with your API key and configuration.

Configuration

FieldTypeRequiredDescriptionExample
apiKeystring
Optional
Your Orvion API key (required)
baseUrlstring?
Optional
Custom API base URL (default: https://api.orvion.sh)
cacheTtlSecondsnumber
Optional
Route cache TTL in seconds (default: 60)
transactionHeaderstring
Optional
Header for transaction ID (default: 'X-Transaction-Id')
customerHeaderstring
Optional
Header for customer ID (default: 'X-Customer-Id')

requirePayment Middleware

Protect Express routes with payment requirements.

Parameters

FieldTypeRequiredDescriptionExample
amountstring?
Optional
Price per request (e.g., '0.10')
currencystring
Optional
Currency code (default: 'USD')
allowAnonymousboolean?
Optional
Allow requests without customer ID (default: true)
customerResolverFunction?
Optional
Function to extract customer ID from request
hostedCheckoutboolean
Optional
Enable hosted checkout mode - redirects to pay.orvion.sh (default: false)
returnUrlstring?
Optional
Return URL for hosted checkout. Auto-derived using convention: /api/foo → /foo

Examples

TypeScript
// Basic usage
app.get('/api/basic',
requirePayment({ amount: '0.01', currency: 'USDC' }),
(req, res) => {
res.json({ data: '...' })
}
)
// With metadata
app.get('/api/premium',
requirePayment({
amount: '1.00',
currency: 'USDC',
}),
(req, res) => {
res.json({ data: '...', paid: req.payment.amount })
}
)
// Require customer identification
app.get('/api/user-data',
requirePayment({
amount: '0.05',
currency: 'USDC',
allowAnonymous: false,
customerResolver: (req) => req.headers['x-user-id'],
}),
(req, res) => {
res.json({ user: req.payment.customerRef })
}
)
// Hosted checkout mode - redirects to pay.orvion.sh
app.get('/premium',
requirePayment({
amount: '1.00',
currency: 'USDC',
hostedCheckout: true,
}),
(req, res) => {
res.json({ message: 'Premium content after hosted checkout!' })
}
)

New Client Methods (v0.2)

The SDK now includes powerful client methods for payment operations:

TypeScript
import { OrvionClient } from '@orvion/sdk'
const client = new OrvionClient({ apiKey: 'your-api-key' })
// Health check & API key validation
const health = await client.healthCheck()
console.log(`Organization: ${health.organizationId}`)
// Create a charge
const charge = await client.createCharge({
amount: '0.10',
currency: 'USDC',
})
// Get charge state (optimized for UI widgets)
const state = await client.getChargeState(charge.id)
console.log(`Status: ${state.status}`)
// Confirm wallet payment
const result = await client.confirmPayment({
transactionId: charge.id,
txHash: 'blockchain_tx_signature...',
})
// Cancel a pending charge
const cancelled = await client.cancelCharge(charge.id)

Accessing Payment Info

After successful payment verification, payment details are available on req.payment:

TypeScript
app.get('/api/premium',
requirePayment({ amount: '0.10', currency: 'USDC' }),
(req, res) => {
const { payment } = req
res.json({
transactionId: payment.transactionId,
amount: payment.amount,
currency: payment.currency,
customerRef: payment.customerRef,
})
}
)

Routing Flows vs Standalone Charges

Each protected route has its own amount stored in the protected_routes table. When a route is accessed, Orvion determines how to process the payment:

With Routing Flows

If you have an active routing flow with an api_request_entry node, the route automatically uses that flow for payment configuration:

  • Amount: Uses the route's amount from protected_routes table
  • Payment Config: Network, asset, and receiver come from the routing flow
  • Use Case: Dynamic routing, conditional logic, A/B testing
TypeScript
// Route with routing flow
app.get('/api/flow',
requirePayment({
amount: '0.0015', // Route's own amount
currency: 'USDC',
}),
(req, res) => {
// Payment config comes from active routing flow
// Amount comes from this route definition
res.json({ message: 'Routed through flow!' })
}
)

Standalone Charges

If there's no active routing flow, the route uses a standalone charge:

  • Amount: Uses the route's amount from protected_routes table
  • Payment Config: Uses the route's receiverConfigId (or default receiver)
  • Use Case: Simple, fixed pricing with direct control
TypeScript
// Route with standalone charge
app.get('/api/premium',
requirePayment({
amount: '0.001', // Route's own amount
currency: 'USDC',
receiverConfigId: 'config_abc123', // Optional: specific receiver config
}),
(req, res) => {
// Payment config comes from receiverConfigId or default
// Amount comes from this route definition
res.json({ message: 'Premium content!' })
}
)

Key Point: Every protected route stores its own amount. The difference between routing flows and standalone charges is only in how the payment configuration (network, asset, receiver) is determined—not the amount itself.


Complete Example

TypeScript
import express from 'express'
import { OrvionClient } from '@orvion/sdk'
import {
orvionInit,
requirePayment,
createPaymentRouter,
} from '@orvion/sdk/express'
const app = express()
app.use(express.json())
const client = new OrvionClient({ apiKey: process.env.ORVION_API_KEY! })
// Initialize Orvion
orvionInit({ apiKey: process.env.ORVION_API_KEY! })
// Add pre-built payment router
app.use('/api/payments', createPaymentRouter(client))
// Free endpoint
app.get('/', (req, res) => {
res.json({ message: 'Welcome! Visit /api/premium for paid content.' })
})
// Premium endpoint - 0.01 USDC
app.get('/api/premium',
requirePayment({
amount: '0.01',
currency: 'USDC',
}),
(req, res) => {
res.json({
message: 'Welcome to premium!',
paid: req.payment.amount,
})
}
)
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})

TypeScript Types

TypeScript
import type { PaymentInfo } from '@orvion/sdk/express'
interface RequestWithPayment extends express.Request {
payment: PaymentInfo
}
app.get('/api/premium',
requirePayment({ amount: '0.10', currency: 'USDC' }),
(req: RequestWithPayment, res) => {
// TypeScript knows about req.payment
const transactionId: string = req.payment.transactionId
const amount: string = req.payment.amount
res.json({ transactionId, amount })
}
)

Hosted Checkout Mode

For web applications where users interact via browser, use hosted checkout mode:

TypeScript
// API endpoint - protected with payment
app.get('/api/premium',
requirePayment({
amount: '1.00',
currency: 'USDC',
hostedCheckout: true, // Redirect to pay.orvion.sh
}),
(req, res) => {
res.json({ message: 'Premium content!' })
}
)
// Frontend page - serves the UI
app.get('/premium', (req, res) => {
res.sendFile('static/premium.html')
})

Automatic URL Convention

The SDK uses a convention-based approach for seamless UX:

  • API at /api/premiumFrontend at /premium
  • The /api prefix is automatically stripped
  • Users are redirected to the frontend page after payment (not the API)

No returnUrl configuration needed! Just add hostedCheckout: true and follow the convention.

TypeScript
// That's all you need! No returnUrl required.
app.get('/api/premium',
requirePayment({ amount: '1.00', currency: 'USDC', hostedCheckout: true }),
(req, res) => {
res.json({ data: 'premium' })
}
)
// Flow:
// 1. User on /premium clicks "Pay" → redirects to /api/premium
// 2. SDK creates charge, redirects to pay.orvion.sh
// 3. After payment, user returns to /api/premium?charge_id=xxx
// 4. SDK verifies, then redirects to /premium?charge_id=xxx&status=succeeded
// 5. Frontend shows success message

Before using hosted checkout, configure allowed domains in Settings → Domains to enable return URL validation.

See the Hosted Checkout Guide for complete documentation.


Related Documentation