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)

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:

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_checkoutboolOptionalEnable hosted checkout mode (default: False)
return_urlstr?OptionalExplicit return URL. If not provided, auto-derived using convention: /api/foo → /foo
amountstr?OptionalPrice per request (e.g., '1.00')
currencystrOptionalCurrency code (default: 'USD')

Node.js: requirePayment

FieldTypeRequiredDescriptionExample
hostedCheckoutbooleanOptionalEnable hosted checkout mode (default: false)
returnUrlstring?OptionalExplicit return URL. If not provided, auto-derived using convention: /api/foo → /foo
amountstring?OptionalPrice per request (e.g., '1.00')
currencystringOptionalCurrency 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:

Python
@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:

  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:

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:

Response:

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

Related Documentation