Session-Based Checkout
Sessions let you create a secure, tamper-proof checkout from your backend using a secret key (sk_), then redirect users to complete payment. This is ideal when you need server-side control over transaction parameters — locking the wallet address, amount, or currency so the end user can’t change them.
There are two variants:
| Variant | Description | UI Work |
|---|
| Hosted Checkout | Redirect to checkout.nowramp.com — we handle the entire UI | None |
| Custom UI | Build your own checkout page using session + onramp API | Full control |
How It Works
Step 1: Create a Session
Create a session from your backend using your secret key (sk_). This is the only step that requires a secret key — everything else uses public keys or no auth.
Request
curl -X POST https://api.nowramp.com/v1/sessions \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "player_12345",
"sourceAmount": "100",
"sourceCurrency": "USD",
"destinationCurrency": "ETH",
"network": "ethereum",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"redirectUrl": "https://yoursite.com/payment-complete",
"type": "onramp",
"metadata": {
"partnerOrderId": "game_purchase_789",
"itemId": "sword_of_destiny"
}
}'
Session metadata flows into order.metadata.partnerMetadata when an order is created. The same data is included in order.created and order.completed webhook payloads, so you can correlate orders back to your system without extra lookups.
Response (201 Created)
{
"type": "session",
"id": "sess_abc123",
"attributes": {
"sessionId": "sess_abc123",
"projectId": "550e8400-e29b-41d4-a716-446655440000",
"clientSecret": "sess_secret_xyz789",
"type": "onramp",
"status": "pending",
"customerId": "cust_auto_created",
"externalCustomerId": "player_12345",
"sourceAmount": "100",
"sourceCurrency": "USD",
"destinationCurrency": "ETH",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"network": "ethereum",
"redirectUrl": "https://yoursite.com/payment-complete",
"fieldLocks": {
"sourceAmount": { "locked": true },
"sourceCurrency": { "locked": true },
"destinationCurrency": { "locked": true },
"destinationAddress": { "locked": true },
"network": { "locked": true }
},
"expiresAt": "2026-02-17T15:30:00Z",
"createdAt": "2026-02-17T15:00:00Z"
}
}
Save the clientSecret immediately. It is only returned once on creation and cannot be retrieved later. You’ll need it to redirect users and authorize frontend operations.
Session Fields
| Field | Required | Description |
|---|
externalCustomerId | Yes* | Your user/player ID. At least one of externalCustomerId or customerId is required. |
customerId | Yes* | NowRamp customer UUID. If you provide externalCustomerId instead, a customer is auto-created. |
type | No | "onramp" (default) or "offramp" |
sourceAmount | No | Fiat amount (e.g., "100") |
sourceCurrency | No | Fiat currency code (e.g., "USD", "EUR") |
destinationCurrency | No | Crypto currency code (e.g., "ETH", "BTC", "USDC") |
network | No | Blockchain network (e.g., "ethereum", "polygon", "bitcoin") |
destinationAddress | No | Wallet address to receive crypto |
redirectUrl | No | URL to redirect the user after payment completes |
country | No | ISO 2-letter country code (auto-detected from IP if omitted) |
expiresInSeconds | No | Session TTL. Default: 1800 (30 min). Max: 86400 (24 hours). |
metadata | No | Partner data attached to the session. Propagates to order.metadata.partnerMetadata when an order is created, and is included in webhook payloads. Max 20 keys, 4KB, 2 levels of nesting. |
fieldLocks | No | Explicit field lock overrides (see below) |
Auto-Locking
When you provide a field value in the session, it is automatically locked — the user cannot change it in the checkout UI. For example, if you set destinationAddress, the wallet address field will be read-only.
You can override this behavior with explicit fieldLocks:
{
"destinationAddress": "0x742d...",
"fieldLocks": {
"destinationAddress": { "locked": false }
}
}
Step 2: Redirect to Checkout
After creating the session, you have two options.
Option A: Hosted Checkout (Recommended)
Redirect the user to our hosted checkout page. We handle the full checkout UI — currency selection, quote comparison, payment, and redirect.
https://checkout.nowramp.com?sessionId={sessionId}&clientSecret={clientSecret}&projectId={projectId}
Example redirect from your backend:
// Node.js / Express
app.post('/buy-crypto', async (req, res) => {
const session = await fetch('https://api.nowramp.com/v1/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOWRAMP_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalCustomerId: req.user.id,
sourceAmount: '100',
sourceCurrency: 'USD',
destinationCurrency: 'ETH',
network: 'ethereum',
destinationAddress: req.user.walletAddress,
redirectUrl: `${process.env.APP_URL}/payment-complete`,
type: 'onramp',
}),
}).then(r => r.json());
const { sessionId, clientSecret, projectId } = session.attributes;
const checkoutUrl = new URL('https://checkout.nowramp.com');
checkoutUrl.searchParams.set('sessionId', sessionId);
checkoutUrl.searchParams.set('clientSecret', clientSecret);
checkoutUrl.searchParams.set('projectId', projectId);
res.redirect(302, checkoutUrl.toString());
});
# Python / Flask
@app.route('/buy-crypto', methods=['POST'])
def buy_crypto():
session = requests.post('https://api.nowramp.com/v1/sessions', json={
'externalCustomerId': current_user.id,
'sourceAmount': '100',
'sourceCurrency': 'USD',
'destinationCurrency': 'ETH',
'network': 'ethereum',
'destinationAddress': current_user.wallet_address,
'redirectUrl': f'{APP_URL}/payment-complete',
'type': 'onramp',
}, headers={
'Authorization': f'Bearer {NOWRAMP_SECRET_KEY}',
'Content-Type': 'application/json',
}).json()
attrs = session['attributes']
checkout_url = (
f"https://checkout.nowramp.com"
f"?sessionId={attrs['sessionId']}"
f"&clientSecret={attrs['clientSecret']}"
f"&projectId={attrs['projectId']}"
)
return redirect(checkout_url, code=302)
That’s it. The hosted checkout page handles everything:
- Loads the session and applies field locks
- Shows available currencies and payment methods (filtered by user’s country)
- Fetches live quotes from all enabled gateways
- Lets the user select a gateway and confirm
- Redirects to the payment gateway for checkout
- After payment, redirects back to your
redirectUrl
What Is Hosted Checkout?
Hosted checkout is a pre-built, NowRamp-hosted checkout page that partners can redirect users to instead of building their own UI. Think of it like Stripe Checkout — you create a session on your server and send the user to our page to complete payment.
Benefits:
- Zero frontend work — no React components or SDK needed
- Always up to date — new gateways, currencies, and features are available immediately
- Responsive — works on mobile and desktop
- Partner branding — custom logo, colors, and background per project
- Secure — session parameters are locked server-side, preventing client-side tampering
When to use hosted checkout:
- You want the fastest possible integration
- You don’t need a custom checkout UI
- You’re building a backend-only integration (mobile apps, game servers, etc.)
- You want to redirect users from any platform (web, mobile, email links)
Option B: Custom UI
If you want full control over the checkout experience, use the session endpoints combined with the public onramp API to build your own UI.
2a. Load the Session (frontend)
curl https://api.nowramp.com/v1/sessions/sess_abc123 \
-H "Authorization: Bearer pk_live_YOUR_PUBLIC_KEY"
This returns the session details (amount, currency, wallet, field locks) so you can populate your UI. Locked fields should be displayed as read-only.
2b. Fetch Quotes (no auth needed)
curl "https://api.nowramp.com/public/v1/onramp/quotes?\
projectId=YOUR_PROJECT_UUID&\
fiatCurrency=USD&\
cryptoCurrency=ETH&\
network=ethereum&\
fiatAmount=100"
Returns ranked quotes from all enabled gateways. Display these to the user so they can pick a provider.
2c. Create Order from Session
When the user selects a quote and confirms, create the order:
curl -X POST https://api.nowramp.com/v1/sessions/sess_abc123/order \
-H "Authorization: Bearer pk_live_YOUR_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "sess_secret_xyz789",
"fiatAmount": "100",
"fiatCurrency": "USD",
"cryptoCurrency": "ETH",
"network": "ethereum",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"gateway": "gateway_a",
"email": "player@example.com"
}'
Response (201 Created):
{
"type": "order",
"id": "ord_xyz",
"attributes": {
"orderId": "ord_xyz",
"status": "created",
"sessionId": "sess_abc123",
"checkout": {
"method": "redirect",
"url": "https://checkout.gateway-a.com/...",
"expiresAt": 1800
}
}
}
2d. Redirect to Payment Gateway
Open the checkout.url in the user’s browser:
window.location.href = order.attributes.checkout.url;
Or embed it in an iframe if the gateway supports it (check checkout.method):
| Method | How to Use |
|---|
redirect | Navigate the browser to checkout.url |
iframe | Load checkout.url in an <iframe> |
widget | Initialize the gateway’s JavaScript widget |
After payment, the gateway redirects the user back to the redirectUrl from the session.
Step 3: Track the Transaction
Option A: Webhooks (Recommended)
Configure a webhook URL in the Partner Dashboard to receive real-time notifications:
{
"type": "transaction.completed",
"data": {
"transaction": {
"id": "txn_xyz789",
"status": "completed",
"fiatAmount": "100.00",
"cryptoAmount": "0.0523",
"transactionHash": "0xabc123..."
}
}
}
See the Callback URLs guide for full setup.
Option B: Poll the Transaction Status
curl "https://api.nowramp.com/public/v1/onramp/transactions/ord_xyz"
Authentication Summary
| Step | Key Type | Endpoint |
|---|
| Create session | sk_ (secret key) | POST /v1/sessions |
| Load session | pk_ (public key) | GET /v1/sessions/:sessionId |
| Confirm session | pk_ + clientSecret | POST /v1/sessions/:sessionId/confirm |
| Get quotes | None | GET /public/v1/onramp/quotes |
| Get supported | None | GET /public/v1/onramp/supported |
| Create order | pk_ + clientSecret | POST /v1/sessions/:sessionId/order |
| Cancel session | sk_ (secret key) | POST /v1/sessions/:sessionId/cancel |
Secret keys (sk_) must never be exposed in client-side code. Only use them from your backend server. Public keys (pk_) are safe for frontend use.
Session Lifecycle
| Status | Description |
|---|
pending | Session created, awaiting user action |
confirmed | User confirmed session parameters (optional step) |
processing | Order created, payment in progress |
completed | Payment and crypto delivery successful |
expired | Session expired before use (default: 30 minutes) |
cancelled | Session cancelled by partner backend |
failed | Payment or delivery failed |
Error Handling
| Error | HTTP | Description |
|---|
SESSION_EXPIRED | 409 | Session has expired. Create a new one. |
SESSION_ALREADY_USED | 409 | An order was already created from this session. |
FORBIDDEN | 403 | Invalid clientSecret. |
VALIDATION_ERROR | 400 | Missing or invalid fields in the request body. |
NOT_FOUND | 404 | Session ID does not exist. |
{
"success": false,
"error": {
"code": "SESSION_EXPIRED",
"message": "Session has expired"
}
}
Full Example: Hosted Checkout (curl)
# 1. Your backend creates a session
RESPONSE=$(curl -s -X POST https://api.nowramp.com/v1/sessions \
-H "Authorization: Bearer sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "player_999",
"sourceAmount": "50",
"sourceCurrency": "USD",
"destinationCurrency": "USDC",
"network": "polygon",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"redirectUrl": "https://yoursite.com/success",
"type": "onramp"
}')
# 2. Extract the values you need
SESSION_ID=$(echo $RESPONSE | jq -r '.attributes.sessionId')
CLIENT_SECRET=$(echo $RESPONSE | jq -r '.attributes.clientSecret')
PROJECT_ID=$(echo $RESPONSE | jq -r '.attributes.projectId')
# 3. Build the redirect URL
echo "https://checkout.nowramp.com?sessionId=$SESSION_ID&clientSecret=$CLIENT_SECRET&projectId=$PROJECT_ID"
# 4. Redirect your user to that URL — done!
Environments
| Environment | API URL | Checkout URL |
|---|
| Sandbox | https://api.sandbox.nowramp.com | https://checkout.sandbox.nowramp.com |
| Production | https://api.nowramp.com | https://checkout.nowramp.com |
Use sandbox for testing. All the same endpoints work with sk_test_ / pk_test_ keys.
A common need is to attach your own order ID, user ID, or other tracking data to a session so you can match it back when you receive webhooks. Use the metadata field on session creation — it flows through the entire pipeline automatically.
curl -X POST https://api.nowramp.com/v1/sessions \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "player_12345",
"sourceAmount": "100",
"sourceCurrency": "USD",
"destinationCurrency": "ETH",
"network": "ethereum",
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"redirectUrl": "https://yoursite.com/payment-complete",
"type": "onramp",
"metadata": {
"orderId": "your-internal-order-123",
"userId": "player_12345"
}
}'
Step 2: User completes checkout
Your metadata is stored on the session. When an order is created (either via hosted checkout or your custom UI), the metadata is automatically propagated to the order as partnerMetadata.
Step 3: Receive it in webhooks
When the order completes (or fails, is refunded, etc.), your webhook receives the full metadata:
{
"type": "order.completed",
"data": {
"order": {
"id": "ord_xyz789",
"type": "buy",
"status": "completed",
"source": { "currency": "USD", "amount": "100.00" },
"destination": {
"currency": "ETH",
"amount": "0.0523",
"address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21"
},
"customerId": "cust_uuid",
"externalCustomerId": "player_12345",
"metadata": {
"provider": "banxa",
"providerOrderId": "banxa-order-abc",
"email": "player@example.com",
"partnerMetadata": {
"orderId": "your-internal-order-123",
"userId": "player_12345"
}
},
"createdAt": "2026-02-25T10:00:00.000Z",
"completedAt": "2026-02-25T10:05:00.000Z"
}
}
}
Step 4: Match it back in your webhook handler
app.post('/webhooks/ramp', (req, res) => {
res.status(200).json({ received: true });
const order = req.body.data.order;
const partnerData = order.metadata?.partnerMetadata;
if (partnerData?.orderId) {
// Update your internal order with the ramp result
db.orders.update({
where: { id: partnerData.orderId },
data: {
rampOrderId: order.id,
rampStatus: order.status,
cryptoAmount: order.destination.amount,
}
});
}
});
| Constraint | Limit |
|---|
| Max top-level keys | 20 |
| Max serialized size | 4KB |
| Max nesting depth | 2 levels |
// PASSES: { "orderId": "abc", "tags": { "env": "prod" } } → depth 1
// PASSES: { "orderId": "abc", "ctx": { "org": { "id": "x" } } } → depth 2
// FAILS: { "a": { "b": { "c": { "d": 1 } } } } → depth 3
Also works with direct checkout-intent
If you’re using the onramp API directly (without sessions), pass partnerMetadata on the checkout-intent request:
curl -X POST https://api.nowramp.com/public/v1/onramp/checkout-intent \
-H "Content-Type: application/json" \
-d '{
"projectId": "your-project-uuid",
"gateway": "banxa",
"fiatCurrency": "USD",
"fiatAmount": "100",
"cryptoCurrency": "ETH",
"network": "ethereum",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21",
"partnerMetadata": {
"orderId": "your-internal-order-123"
}
}'
System keys (provider, providerOrderId, email, paymentMethodId) are reserved at the top level of order metadata. Your data is always safely namespaced under partnerMetadata — even if you pass keys with the same names, they won’t overwrite the system values.
Next Steps