Hosted Checkout
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
- User visits frontend page (e.g.,
/premium) and clicks "Pay" - Frontend redirects to API (e.g.,
/api/premium) - SDK creates charge with
return_urlauto-derived as frontend path (/premium) - User redirected to
pay.orvion.sh/checkout/{charge_id} - User connects wallet and pays on-chain (Solana/USDC)
- User redirected back to API with
?charge_id=xxx&status=succeeded - 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)
from fastapi import FastAPI, Requestfrom orvion.fastapi import OrvionMiddleware, require_paymentimport osapp = 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)
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:
- Go to Settings → Domains
- Add your domain(s):
https://yourapp.com,http://localhost:3000
Via API:
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
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| hosted_checkout | bool | Optional | Enable hosted checkout mode (default: False) | — |
| return_url | str? | Optional | Explicit return URL. If not provided, auto-derived using convention: /api/foo → /foo | — |
| amount | str? | Optional | Price per request (e.g., '1.00') | — |
| currency | str | Optional | Currency code (default: 'USD') | — |
Node.js: requirePayment
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| hostedCheckout | boolean | Optional | Enable hosted checkout mode (default: false) | — |
| returnUrl | string? | Optional | Explicit return URL. If not provided, auto-derived using convention: /api/foo → /foo | — |
| amount | string? | Optional | Price per request (e.g., '1.00') | — |
| currency | string | 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
/apiprefix 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!
# 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:
2. Response Includes checkout_url
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:
- Extracts
charge_idfrom query params - Calls
POST /v1/charges/verifyto confirm payment - If verified, redirects to the frontend page (e.g.,
/premium?charge_id=xxx&status=succeeded) - 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:
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_urlvalidation 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=oldfrom a previous attempt, it won't be included in the newreturn_url
API Reference
POST /v1/charges (with return_url)
Creates a charge with hosted checkout support.
Request:
Response:
Allowed Domains API
See Configuration section above for examples.
Examples
Basic Web App
from fastapi import FastAPI, Requestfrom fastapi.responses import HTMLResponsefrom orvion.fastapi import OrvionMiddleware, require_paymentimport osapp = 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,}