> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nowramp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Session-Based Checkout

> Create secure checkout sessions server-side and redirect users to complete payment — with our hosted checkout or your own custom UI.

# 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

```mermaid theme={null}
sequenceDiagram
    participant Backend as Your Backend
    participant API as NowRamp API
    participant User as User Browser
    participant Checkout as Hosted Checkout
    participant GW as Payment Gateway

    Backend->>API: POST /v1/sessions (sk_ key)
    API-->>Backend: sessionId + clientSecret

    Backend->>User: Redirect to checkout URL

    alt Hosted Checkout
        User->>Checkout: Open checkout.nowramp.com
        Checkout->>API: Load session (pk_ key)
        Checkout->>API: GET /onramp/quotes
        Checkout->>API: POST /sessions/:id/order (pk_ + clientSecret)
        API-->>Checkout: Checkout URL
        Checkout->>GW: Redirect to gateway
    else Custom UI
        User->>User: Your checkout page
        User->>API: GET /onramp/quotes
        User->>API: POST /sessions/:id/order (pk_ + clientSecret)
        API-->>User: Checkout URL
        User->>GW: Redirect to gateway
    end

    GW-->>User: Redirect to redirectUrl
```

***

## 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

```bash theme={null}
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"
    }
  }'
```

<Info>
  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.
</Info>

### Response (201 Created)

```json theme={null}
{
  "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"
  }
}
```

<Warning>
  **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.
</Warning>

### 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`:

```json theme={null}
{
  "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:**

```javascript theme={null}
// 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 theme={null}
# 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:

1. Loads the session and applies field locks
2. Shows available currencies and payment methods (filtered by user's country)
3. Fetches live quotes from all enabled gateways
4. Lets the user select a gateway and confirm
5. Redirects to the payment gateway for checkout
6. 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)

```bash theme={null}
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)

```bash theme={null}
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:

```bash theme={null}
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):**

```json theme={null}
{
  "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:

```javascript theme={null}
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](https://dashboard.nowramp.com) to receive real-time notifications:

```json theme={null}
{
  "type": "transaction.completed",
  "data": {
    "transaction": {
      "id": "txn_xyz789",
      "status": "completed",
      "fiatAmount": "100.00",
      "cryptoAmount": "0.0523",
      "transactionHash": "0xabc123..."
    }
  }
}
```

See the [Callback URLs guide](/guides/callback-urls) for full setup.

### Option B: Poll the Transaction Status

```bash theme={null}
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`  |

<Warning>
  **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.
</Warning>

***

## Session Lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: POST /v1/sessions
    pending --> confirmed: POST /sessions/:id/confirm
    pending --> processing: POST /sessions/:id/order
    confirmed --> processing: POST /sessions/:id/order

    pending --> cancelled: POST /sessions/:id/cancel
    pending --> expired: TTL exceeded

    processing --> completed: Payment + delivery done
    processing --> failed: Payment failed

    completed --> [*]
    failed --> [*]
    cancelled --> [*]
    expired --> [*]
```

| 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.                      |

```json theme={null}
{
  "success": false,
  "error": {
    "code": "SESSION_EXPIRED",
    "message": "Session has expired"
  }
}
```

***

## Full Example: Hosted Checkout (curl)

```bash theme={null}
# 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.

***

## Passing Your Order ID (Partner Metadata)

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.

### Step 1: Attach metadata when creating the session

```bash theme={null}
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:

```json theme={null}
{
  "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

```javascript theme={null}
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,
      }
    });
  }
});
```

### Metadata limits

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

```bash theme={null}
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"
    }
  }'
```

<Info>
  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.
</Info>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Callback URLs" icon="webhook" href="/guides/callback-urls">
    Set up webhooks to track transaction status in real-time
  </Card>

  <Card title="Onramp API" icon="code" href="/api/onramp">
    Full API reference for quotes, checkout, and transactions
  </Card>

  <Card title="Authentication" icon="key" href="/guides/authentication">
    API key types, rate limits, and security best practices
  </Card>

  <Card title="React Components" icon="react" href="/sdk/form">
    Drop-in checkout form if you want an embedded UI instead
  </Card>
</CardGroup>
