Hosted Checkout

Beta

Hosted Checkout provides a turnkey payment experience for your buyers. Instead of returning a 402 response with payment requirements, redirect buyers to pay.orvion.sh where they can connect their wallet and complete payment—then get redirected back to your app.

Overview

Two Payment Modes:

| Mode | How it Works | Best For | |------|--------------|----------| | 402 Mode (default) | Returns HTTP 402 with x402 payment requirements | API clients, programmatic access | | Hosted Checkout | Redirects to pay.orvion.sh, then back to your app | Web apps, browser-based users |

Hosted Checkout is ideal for web applications where users interact via browser. The 402 mode remains the default for backward compatibility with API clients.

How It Works

User clicks Pay → API Endpoint → Redirect to pay.orvion.sh → User Pays → Redirect to API → Verify → Redirect to Frontend
     ↓                ↓                    ↓                                    ↓                        ↓
  /premium      /api/premium        Wallet Connect                      ?charge_id=xxx         /premium?status=succeeded
                                    USDC Payment
  1. User visits frontend page (e.g., /premium) and clicks "Pay"
  2. Frontend redirects to API (e.g., /api/premium)
  3. SDK creates charge with return_url auto-derived as frontend path (/premium)
  4. User redirected to pay.orvion.sh/checkout/{charge_id}
  5. User connects wallet and pays on-chain (Solana/USDC)
  6. User redirected back to API with ?charge_id=xxx&status=succeeded
  7. SDK verifies payment and redirects to frontend with success params

Zero configuration needed! The SDK automatically derives /premium from /api/premium and redirects users to your frontend page after payment—not the API endpoint returning JSON.

Quick Start

Python (FastAPI)

Python
from fastapi import FastAPI, Request
from orvion.fastapi import OrvionMiddleware, require_payment
import os
app = FastAPI()
app.add_middleware(OrvionMiddleware, api_key=os.environ["ORVION_API_KEY"])
# Hosted Checkout mode - redirects to pay.orvion.sh
@app.get("/premium")
@require_payment(amount="1.00", currency="USDC", hosted_checkout=True)
async def premium(request: Request):
return {"message": "Welcome to premium!", "paid": request.state.payment.amount}

Node.js (Express)

TypeScript
import express from 'express'
import { orvionInit, requirePayment } from '@orvion/sdk/express'
const app = express()
orvionInit({ apiKey: process.env.ORVION_API_KEY! })
// Hosted Checkout mode - redirects to pay.orvion.sh
app.get('/premium',
requirePayment({ amount: '1.00', currency: 'USDC', hostedCheckout: true }),
(req, res) => {
res.json({ message: 'Welcome to premium!', paid: req.payment.amount })
}
)

Configuration

Allowed Domains (Required)

Before using hosted checkout, you must configure allowed domains for your organization. This prevents open redirect attacks by validating the return_url.

Via Dashboard:

  1. Go to Settings → Domains
  2. Add your domain(s): https://yourapp.com, http://localhost:3000

Via API:

Bash
# Add an allowed domain
curl -X POST https://api.orvion.sh/v1/organizations/allowed-domains \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "https://yourapp.com"}'
# List allowed domains
curl https://api.orvion.sh/v1/organizations/allowed-domains \
-H "Authorization: Bearer YOUR_API_KEY"
# Remove a domain
curl -X DELETE "https://api.orvion.sh/v1/organizations/allowed-domains/https%3A%2F%2Fyourapp.com" \
-H "Authorization: Bearer YOUR_API_KEY"

If you don't configure allowed domains, charge creation will fail with invalid_return_url error when using hosted checkout.

SDK Parameters

Python: @require_payment

FieldTypeRequiredDescriptionExample
hosted_checkoutbool
Optional
Enable hosted checkout mode (default: False)
return_urlstr?
Optional
Explicit return URL. If not provided, auto-derived using convention: /api/foo → /foo
amountstr?
Optional
Price per request (e.g., '1.00')
currencystr
Optional
Currency code (default: 'USD')

Node.js: requirePayment

FieldTypeRequiredDescriptionExample
hostedCheckoutboolean
Optional
Enable hosted checkout mode (default: false)
returnUrlstring?
Optional
Explicit return URL. If not provided, auto-derived using convention: /api/foo → /foo
amountstring?
Optional
Price per request (e.g., '1.00')
currencystring
Optional
Currency code (default: 'USD')

Return URL Handling

Automatic Frontend Detection (Recommended)

The SDK automatically derives the return URL using a convention-based approach:

  • If your API is at /api/premium, the SDK assumes your frontend is at /premium
  • The /api prefix is automatically stripped to derive the frontend URL
  • After payment verification, users are redirected to the frontend page (not the API)

This means zero configuration is needed for most setups!

Python
# Your API endpoint
@app.get("/api/premium")
@require_payment(amount="1.00", currency="USDC", hosted_checkout=True)
async def premium(request: Request):
return {"data": "premium"}
# Your frontend page (served separately)
@app.get("/premium")
async def premium_page():
return FileResponse("static/premium.html")
# Flow:
# 1. User visits /premium (frontend)
# 2. Clicks "Pay" → goes to /api/premium
# 3. SDK redirects to pay.orvion.sh with return_url=/premium
# 4. After payment, user returns to /premium?charge_id=xxx&status=succeeded
# 5. SDK verifies and redirects to /premium (frontend) with success params

Convention: /api/foo/foo. The SDK strips the /api prefix automatically. Your frontend page should be at the path without /api.

Explicit Return URL

For custom flows where the convention doesn't apply, provide an explicit return_url:

@app.get("/checkout")
@require_payment(
amount="5.00",
currency="USDC",
hosted_checkout=True,
return_url="https://yourapp.com/success" # Custom return URL
)
async def checkout(request: Request):
return {"status": "paid"}

Query Parameters on Return

When the buyer returns from checkout, these query parameters are appended:

| Parameter | Description | |-----------|-------------| | charge_id | The charge/transaction ID | | status | Payment status: succeeded or cancelled |

Example: https://yourapp.com/premium?charge_id=abc123&status=succeeded

Payment Flow Details

1. Create Charge with return_url

When hosted_checkout=True, the SDK calls POST /v1/charges with a return_url:

JSON
{
"amount": "1.00",
"currency": "USDC",
"customer_ref": "user_123",
"resource_ref": "protected_route:abc",
"return_url": "https://yourapp.com/premium"
}

2. Response Includes checkout_url

JSON
{
"id": "txn_abc123",
"amount": "1.00",
"currency": "USDC",
"status": "pending",
"checkout_url": "https://pay.orvion.sh/checkout/txn_abc123",
"return_url": "https://yourapp.com/premium",
"x402_requirements": { ... }
}

3. SDK Redirects to checkout_url

The SDK issues an HTTP 302 redirect to checkout_url.

4. Buyer Completes Payment

At pay.orvion.sh/checkout/{id}:

  • Buyer connects Solana wallet
  • Sees payment amount and merchant info
  • Signs and submits USDC transfer
  • Transaction confirmed on-chain

5. Redirect Back with charge_id

After successful payment, buyer is redirected to:

https://yourapp.com/premium?charge_id=txn_abc123&status=succeeded

6. SDK Verifies and Redirects to Frontend

On the return request, the SDK:

  1. Extracts charge_id from query params
  2. Calls POST /v1/charges/verify to confirm payment
  3. If verified, redirects to the frontend page (e.g., /premium?charge_id=xxx&status=succeeded)
  4. If not verified, creates a new charge and redirects to checkout again

Important: After verification, users are redirected to the frontend page—not the API endpoint. This ensures users see your nice UI instead of raw JSON.

Error Handling

invalid_return_url

If the return_url origin is not in your allowed domains:

JSON
{
"error": "invalid_return_url",
"detail": "return_url origin 'https://evil.com' is not in allowed domains. Add 'https://evil.com' to allowed domains in your dashboard settings."
}

Solution: Add the domain to Settings → Domains in your dashboard.

No checkout_url in Response

If the backend doesn't return a checkout_url, the SDK returns a 500 error. This typically means:

  • return_url validation failed
  • Server configuration issue

Payment Verification Failed

If verification fails on return, the SDK creates a new charge and redirects again. This handles edge cases like:

  • Browser back button after failed payment
  • Expired or cancelled charges

Security Considerations

Domain Allowlist

The return_url must match an allowed domain for your organization. This prevents:

  • Open redirects: Attackers can't redirect users to malicious sites
  • Phishing: Users always return to legitimate seller domains

Resource Reference

Each charge includes a resource_ref (protected_route:{id}) that binds the payment to a specific route. This prevents:

  • Payment reuse: Can't use a payment for route A to access route B
  • Price manipulation: Can't pay for a cheap route and access an expensive one

Query Parameter Stripping

When deriving return_url, the SDK strips existing query parameters. This prevents:

  • Duplicate charge_id: If user has ?charge_id=old from a previous attempt, it won't be included in the new return_url

API Reference

POST /v1/charges (with return_url)

Creates a charge with hosted checkout support.

Request:

JSON
{
"amount": "1.00",
"currency": "USDC",
"customer_ref": "user_123",
"resource_ref": "protected_route:abc",
"return_url": "https://yourapp.com/premium"
}

Response:

JSON
{
"id": "txn_abc123",
"amount": "1.00",
"currency": "USDC",
"status": "pending",
"checkout_url": "https://pay.orvion.sh/checkout/txn_abc123",
"return_url": "https://yourapp.com/premium",
"x402_requirements": {
"rail_config": {
"scheme": "exact",
"network": "solana-mainnet",
"asset": "USDC",
"pay_to_address": "..."
},
"amount": "1.00",
"currency": "USDC"
}
}

Allowed Domains API

See Configuration section above for examples.

Examples

Basic Web App

Python
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from orvion.fastapi import OrvionMiddleware, require_payment
import os
app = FastAPI()
app.add_middleware(OrvionMiddleware, api_key=os.environ["ORVION_API_KEY"])
@app.get("/", response_class=HTMLResponse)
async def home():
return """
<html>
<body>
<h1>Welcome!</h1>
<a href="/premium">Access Premium Content ($1.00)</a>
</body>
</html>
"""
@app.get("/premium")
@require_payment(amount="1.00", currency="USDC", hosted_checkout=True)
async def premium(request: Request):
return {
"message": "You've unlocked premium content!",
"charge_id": request.state.payment.transaction_id,
}

With Custom Return URL

TypeScript
import express from 'express'
import { orvionInit, requirePayment } from '@orvion/sdk/express'
const app = express()
orvionInit({ apiKey: process.env.ORVION_API_KEY! })
// Checkout initiator
app.get('/buy/:productId', (req, res) => {
// Store product in session, then redirect to payment
req.session.productId = req.params.productId
res.redirect('/pay')
})
// Payment route with custom return
app.get('/pay',
requirePayment({
amount: '9.99',
currency: 'USDC',
hostedCheckout: true,
returnUrl: 'https://yourapp.com/success'
}),
(req, res) => {
// This only runs after successful payment
res.redirect('/success')
}
)
// Success page
app.get('/success', (req, res) => {
const chargeId = req.query.charge_id
res.json({
message: 'Payment successful!',
chargeId,
productId: req.session.productId
})
})

Related Documentation