Decoupled checkout sessions for AnySpend. Sessions are decoupled from orders — create a session first, then create an order when the user is ready to pay.
Flow
Merchant creates session
POST /checkout-sessions returns { id, status: "open" }
User picks payment method
POST /orders { checkoutSessionId } returns { id, globalAddress, oneClickBuyUrl }
User pays
Crypto: send to globalAddress | Onramp: redirect to oneClickBuyUrl
Merchant polls for completion
GET /checkout-sessions/:id returns { status: "complete", order_id }
Why Decoupled?
Session creation is instant (DB-only, no external calls). The order is created separately when the user commits to a payment method.
- Payment method doesn’t need to be known at session creation
- A hosted checkout page can let users choose how to pay
- Session creation never fails due to external API errors
Session Status Lifecycle
| Status | When |
|---|
open | Created, waiting for order/payment |
processing | Payment received, order executing |
complete | Order executed successfully |
expired | TTL expired, payment failed, or manually expired |
API
POST /checkout-sessions — Create Session
Creates a lightweight session. No order, no external API calls.
{
"success_url": "https://merchant.com/success?session_id={SESSION_ID}",
"cancel_url": "https://merchant.com/cancel",
"metadata": { "sku": "widget-1" },
"client_reference_id": "merchant-order-456",
"expires_in": 1800
}
All fields are optional. Payment config (amount, tokens, chains) lives on the order, not the session.
POST /orders — Create Order with Session Linking
Pass checkoutSessionId in the standard order creation request to link the order to a session.
{
"recipientAddress": "0x...",
"srcChain": 8453,
"dstChain": 8453,
"srcTokenAddress": "0x...",
"dstTokenAddress": "0x...",
"srcAmount": "1000000",
"type": "swap",
"payload": { "expectedDstAmount": "1000000" },
"checkoutSessionId": "550e8400-..."
}
Validation:
- Session must exist (
400 if not found)
- Session must be
open (400 if expired/processing/complete)
- Session must not already have an order (
409 Conflict)
GET /checkout-sessions/:id — Retrieve Session
Returns current session state. Status is synced from the underlying order on each retrieval.
| Query Param | Description |
|---|
include=order | Embed the full order object with transactions |
POST /checkout-sessions/:id/expire — Manually Expire
Only works on sessions with status open.
Redirect URL Templates
Use template variables in success_url and cancel_url:
| Variable | Replaced with |
|---|
{SESSION_ID} | The checkout session UUID |
{ORDER_ID} | Same value (alias) |
If no template variable is present, ?sessionId=<uuid> is appended automatically.
SDK Integration
Service Methods
// Create a checkout session
const session = await anyspend.createCheckoutSession({
success_url: "https://mysite.com/success/{SESSION_ID}",
metadata: { sku: "widget-1" },
});
// Retrieve session status
const session = await anyspend.getCheckoutSession(sessionId);
React Hooks
useCreateCheckoutSession
Mutation hook for creating sessions.
import { useCreateCheckoutSession } from "@b3dotfun/sdk/anyspend";
const { mutate: createSession, data, isPending } = useCreateCheckoutSession();
useCheckoutSession
Query hook with auto-polling. Stops polling when status reaches complete or expired.
import { useCheckoutSession } from "@b3dotfun/sdk/anyspend";
const { data: session, isLoading } = useCheckoutSession(sessionId);
Component checkoutSession Prop
The <AnySpend>, <AnySpendCustom>, and <AnySpendCustomExactIn> components accept an optional checkoutSession prop:
<AnySpend
defaultActiveTab="fiat"
destinationTokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
destinationTokenChainId={8453}
recipientAddress="0x..."
checkoutSession={{
success_url: "https://myshop.com/success?session={SESSION_ID}",
cancel_url: "https://myshop.com/cancel",
metadata: { sku: "widget-1" },
}}
/>
When the checkoutSession prop is set, the component automatically creates a session before creating the order, and uses the session’s success_url for redirects. Without the prop, existing flows are unchanged.
Examples
Crypto Payment
// 1. Create session
const session = await fetch("/checkout-sessions", {
method: "POST",
body: JSON.stringify({
success_url: "https://mysite.com/success/{SESSION_ID}",
metadata: { sku: "widget-1" },
}),
}).then(r => r.json());
// 2. Create order linked to session
const order = await fetch("/orders", {
method: "POST",
body: JSON.stringify({
recipientAddress: "0x...",
srcChain: 8453,
dstChain: 8453,
srcTokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
dstTokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
srcAmount: "1000000",
type: "swap",
payload: { expectedDstAmount: "1000000" },
checkoutSessionId: session.data.id,
}),
}).then(r => r.json());
// 3. User sends crypto to order.data.globalAddress
// 4. Poll session until complete
const poll = setInterval(async () => {
const s = await fetch(`/checkout-sessions/${session.data.id}`).then(r => r.json());
if (s.data.status === "complete") {
clearInterval(poll);
// redirect to success_url or show confirmation
}
}, 3000);
Onramp Payment (Coinbase/Stripe)
// Steps 1-2 same as above, but include onramp config in order creation:
const order = await fetch("/orders", {
method: "POST",
body: JSON.stringify({
// ... same order fields ...
checkoutSessionId: session.data.id,
onramp: {
vendor: "coinbase",
payment_method: "card",
country: "US",
},
}),
}).then(r => r.json());
// Redirect user to vendor checkout page
window.location.href = order.data.oneClickBuyUrl;
// After vendor redirects back, poll GET /checkout-sessions/:id for completion