Python SDK
The Orvion Python SDK provides seamless integration with FastAPI for creating payment-protected API endpoints using the x402 protocol.
Quick Start
1. Install the SDK
pip install orvion
2. Add Middleware
from fastapi import FastAPIfrom orvion.fastapi import OrvionMiddlewareimport osapp = FastAPI()# Add the Orvion middlewareapp.add_middleware(OrvionMiddleware,api_key=os.environ["ORVION_API_KEY"])
3. Protect Endpoints
from orvion.fastapi import require_paymentfrom fastapi import Request# Define a protected endpoint with pricing@app.get("/api/premium/data")@require_payment(amount="0.10", currency="USDC")async def premium_data(request: Request):return {"data": "Premium content!","paid": request.state.payment.amount}
Pre-built Payment Router (NEW in v0.2)
Eliminate boilerplate by using the pre-built payment router:
from orvion import OrvionClientfrom orvion.fastapi import create_payment_routerclient = OrvionClient(api_key=os.environ["ORVION_API_KEY"])# Add all payment endpoints with one line!app.include_router(create_payment_router(client),prefix="/api/payments",)# 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
OrvionMiddleware
The middleware handles route registration, payment verification, and request state management.
Configuration
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| api_key | str | Optional | Your Orvion API key (required) | — |
| base_url | str? | Optional | Custom API base URL (default: https://api.orvion.sh) | — |
| cache_ttl_seconds | float | Optional | Route cache TTL (default: 60.0) | — |
| transaction_header | str | Optional | Header for transaction ID (default: 'X-Transaction-Id') | — |
| customer_header | str | Optional | Header for customer ID (default: 'X-Customer-Id') | — |
| register_on_first_request | bool | Optional | Auto-register routes on first request (default: True) | — |
@require_payment Decorator
Mark endpoints as payment-protected with configurable pricing.
Parameters
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| amount | str? | Optional | Price per request (e.g., '0.10') | — |
| currency | str | Optional | Currency code (default: 'USD') | — |
| name | str? | Optional | Friendly name for dashboard/402 response | — |
| description | str? | Optional | Description shown in 402 response | — |
| allow_anonymous | bool? | Optional | Allow requests without customer ID (default: True) | — |
| customer_resolver | Callable? | Optional | Function to extract customer ID from request | — |
| hosted_checkout | bool | Optional | Enable hosted checkout mode - redirects to pay.orvion.sh (default: False) | — |
| return_url | str? | Optional | Return URL for hosted checkout. Auto-derived using convention: /api/foo → /foo | — |
Always place @require_payment under @app.get/@app.post. The decorator order matters!
Examples
# Basic usage@app.get("/api/basic")@require_payment(amount="0.01", currency="USDC")async def basic_endpoint(request: Request):return {"data": "..."}# With metadata@app.get("/api/premium")@require_payment(amount="1.00",currency="USDC",name="Premium API Access",description="Full access to premium features",)async def premium_endpoint(request: Request):return {"data": "..."}# Require customer identification@app.get("/api/user-data")@require_payment(amount="0.05",currency="USDC",allow_anonymous=False,customer_resolver=lambda req: req.headers.get("X-User-Id"),)async def user_data(request: Request):return {"user": request.state.payment.customer_ref}# Hosted checkout mode - redirects to pay.orvion.sh@app.get("/premium")@require_payment(amount="1.00",currency="USDC",hosted_checkout=True,)async def premium_hosted(request: Request):return {"message": "Premium content after hosted checkout!"}
New Client Methods (v0.2)
The SDK now includes powerful client methods for payment operations:
from orvion import OrvionClientasync with OrvionClient(api_key="your-api-key") as client:# Health check & API key validationhealth = await client.health_check()print(f"Organization: {health.organization_id}")# Create a chargecharge = await client.create_charge(amount="0.10",currency="USDC",)# Get charge state (optimized for UI widgets)state = await client.get_charge_state(charge.id)print(f"Status: {state.status}")# Confirm wallet paymentresult = await client.confirm_payment(transaction_id=charge.id,tx_hash="blockchain_tx_signature...",)# Cancel a pending chargecancelled = await client.cancel_charge(charge.id)
Accessing Payment Info
After successful payment verification, payment details are available on request.state.payment:
@app.get("/api/premium")@require_payment(amount="0.10", currency="USDC")async def premium(request: Request):payment = request.state.paymentreturn {"transaction_id": payment.transaction_id,"amount": payment.amount,"currency": payment.currency,"customer_ref": payment.customer_ref,}
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_routestable - Payment Config: Network, asset, and receiver come from the routing flow
- Use Case: Dynamic routing, conditional logic, A/B testing
# Route with routing flow@app.get("/api/flow")@require_payment(amount="0.0015", # Route's own amountcurrency="USDC",name="Flow-Routed Content",)async def flow_api(request: Request):# Payment config comes from active routing flow# Amount comes from this route definitionreturn {"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_routestable - Payment Config: Uses the route's
receiver_config_id(or default receiver) - Use Case: Simple, fixed pricing with direct control
# Route with standalone charge@app.get("/api/premium")@require_payment(amount="0.001", # Route's own amountcurrency="USDC",receiver_config_id="config_abc123", # Optional: specific receiver config)async def premium_api(request: Request):# Payment config comes from receiver_config_id or default# Amount comes from this route definitionreturn {"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.
Route Registration
Routes are automatically registered with Orvion when your app starts.
Explicit Startup Registration
For immediate registration at startup, use sync_routes() in your lifespan:
from contextlib import asynccontextmanagerfrom orvion import OrvionClientfrom orvion.fastapi import OrvionMiddleware, sync_routes@asynccontextmanagerasync def lifespan(app: FastAPI):client = OrvionClient(api_key=os.environ["ORVION_API_KEY"])# Verify API key on startuphealth = await client.health_check()if health.api_key_valid:print(f"✓ Connected to org: {health.organization_id}")# Register routescount = await sync_routes(app, client)print(f"✓ Registered {count} protected routes")yieldawait client.close()app = FastAPI(lifespan=lifespan)app.add_middleware(OrvionMiddleware,api_key=os.environ["ORVION_API_KEY"],register_on_first_request=False, # Already synced)
Complete Example
from fastapi import FastAPI, Requestfrom orvion import OrvionClientfrom orvion.fastapi import (OrvionMiddleware,require_payment,create_payment_router,)import osapp = FastAPI(title="My Premium API")client = OrvionClient(api_key=os.environ["ORVION_API_KEY"])# Add middlewareapp.add_middleware(OrvionMiddleware, api_key=os.environ["ORVION_API_KEY"])# Add pre-built payment routerapp.include_router(create_payment_router(client), prefix="/api/payments")# Free endpoint@app.get("/")async def root():return {"message": "Welcome! Visit /api/premium for paid content."}# Premium endpoint - 0.01 USDC@app.get("/api/premium")@require_payment(amount="0.01",currency="USDC",name="Premium Content",)async def premium(request: Request):return {"message": "Welcome to premium!","paid": request.state.payment.amount,}
Hosted Checkout Mode
For web applications where users interact via browser, use hosted checkout mode:
# API endpoint - protected with payment@app.get("/api/premium")@require_payment(amount="1.00",currency="USDC",hosted_checkout=True, # Redirect to pay.orvion.sh)async def premium_api(request: Request):return {"message": "Premium content!"}# Frontend page - serves the UI@app.get("/premium")async def premium_page():return FileResponse("static/premium.html")
Automatic URL Convention
The SDK uses a convention-based approach for seamless UX:
- API at
/api/premium→ Frontend at/premium - The
/apiprefix is automatically stripped - Users are redirected to the frontend page after payment (not the API)
No return_url configuration needed! Just add hosted_checkout=True and follow the convention.
# That's all you need! No return_url required.@app.get("/api/premium")@require_payment(amount="1.00", currency="USDC", hosted_checkout=True)async def premium(request: Request):return {"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
- Protected Routes Overview - General protected routes guide
- Hosted Checkout - Redirect-based payment flow for web apps
- Python SDK - Full Python SDK reference
- API Reference - API endpoints and schemas