# Checkout sessions

> A checkout session is one payment for one order. Create it on your server, send the buyer to its page or start it for your own checkout, and read its payment to fulfil.

Source: https://railbed.io/docs/api/checkout-sessions/ · Updated: 2026-09-26 · Railbed by DeepWork developer docs

## The checkout session object

| Field | Type | Description |
|---|---|---|
| `id` | string | The session's id, `pay_…`. The same id identifies its [payment](https://railbed.io/docs/api/payments.md) |
| `object` | string | `"checkout_session"` |
| `url` | string | The hosted checkout page for this session. Send the buyer here |
| `status` | string | `open`, `pending`, `paid`, `held`, `failed` or `expired`. See [statuses](https://railbed.io/docs/how-it-works.md#statuses) |
| `livemode` | boolean | `false` for Test mode, `true` for Live mode |
| `amount` | string | The price, as set at creation: `"49.00"` |
| `currency` | string | `USD`, `EUR`, `GBP`, `CAD` or `AUD` |
| `description` | string | What the buyer is paying for, shown on the checkout |
| `reference` | string or null | Your own id for the order |
| `customer_email` | string or null | The buyer's email, lowercased |
| `created` | integer | When it was created, in Unix seconds |
| `expires_at` | integer | When it stops accepting new payments: 24 hours after creation |
| `started_at` | integer or null | When the buyer (or your server) started it. Set once |
| `metadata` | object or null | Your key-value pairs, returned unchanged |

## Create a checkout session

`POST /v1/checkout_sessions`

Creates a session for one order. Nothing is charged and no provider is contacted until the buyer starts paying. Send an [`Idempotency-Key`](https://railbed.io/docs/api.md#idempotency) so a retry returns the same session.

| Field | Type | Description |
|---|---|---|
| `amount` | string · required | The price as a decimal string with at most two decimals, from `"1.00"` to `"100000.00"`. `"49"` and `"49.0"` become `"49.00"`. Numbers, exponents and negative values are refused |
| `currency` | string · required | `USD`, `EUR`, `GBP`, `CAD` or `AUD`, in any letter case |
| `description` | string · required | What the buyer is paying for, up to 120 characters |
| `customer_email` | string or null | The buyer's email. Optional here, but needed before the session can start |
| `reference` | string or null | Your order id, up to 120 characters. Returned on the payment and every webhook. Not unique: Railbed doesn't stop two sessions sharing one |
| `metadata` | object or null | Up to 20 keys (1–40 characters, not starting with `__`) with string values up to 500 characters. For your own ids, never secrets or card details |
| `success_url` | string or null | Where the checkout sends the buyer once the payment is confirmed. `{PAYMENT_ID}` and `{REFERENCE}` are filled in. Up to 1,000 characters |
| `cancel_url` | string or null | Where the checkout's "Cancel and return to …" and "Back to …" links lead (they name your business). Up to 1,000 characters |

Return URLs must be full `http://` or `https://` addresses (Live mode: `https://` only) with no username or password. In Live mode your account needs a payout wallet first, or the request gets `409 no_payout_wallet`.

cURL:

```bash
curl https://pay.railbed.io/v1/checkout_sessions \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1042" \
  -d '{
    "amount": "49.00",
    "currency": "USD",
    "description": "Pro Membership",
    "reference": "order_1042",
    "customer_email": "buyer@example.com",
    "metadata": { "user_id": "player_1042" },
    "success_url":
      "https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}",
    "cancel_url": "https://yourstore.com/cart"
  }'
```

Node.js:

```js
const res = await fetch('https://pay.railbed.io/v1/checkout_sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order_1042',
  },
  body: JSON.stringify({
    amount: '49.00',
    currency: 'USD',
    description: 'Pro Membership',
    reference: 'order_1042',
    customer_email: 'buyer@example.com',
    metadata: { user_id: 'player_1042' },
    success_url:
      'https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}',
    cancel_url: 'https://yourstore.com/cart',
  }),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const session = await res.json();
```

Python:

```python
r = requests.post(
    "https://pay.railbed.io/v1/checkout_sessions",
    headers={
        "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}",
        "Idempotency-Key": "order_1042",
    },
    json={
        "amount": "49.00",
        "currency": "USD",
        "description": "Pro Membership",
        "reference": "order_1042",
        "customer_email": "buyer@example.com",
        "metadata": {"user_id": "player_1042"},
        "success_url": (
            "https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}"
        ),
        "cancel_url": "https://yourstore.com/cart",
    },
    timeout=15,
)
r.raise_for_status()
session = r.json()
```

PHP:

```php
<?php
$ch = curl_init('https://pay.railbed.io/v1/checkout_sessions');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: order_1042',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => '49.00',
    'currency' => 'USD',
    'description' => 'Pro Membership',
    'reference' => 'order_1042',
    'customer_email' => 'buyer@example.com',
    'metadata' => ['user_id' => 'player_1042'],
    'success_url' =>
      'https://yourstore.com/thanks?order={REFERENCE}&payment={PAYMENT_ID}',
    'cancel_url' => 'https://yourstore.com/cart',
  ]),
]);
$session = json_decode(curl_exec($ch), true);
```

Response · response 201 Created:

```json
{
  "id": "pay_7AAiYH0Ykt11ED4hmfiN",
  "object": "checkout_session",
  "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN",
  "status": "open",
  "livemode": false,
  "amount": "49.00",
  "currency": "USD",
  "description": "Pro Membership",
  "reference": "order_1042",
  "customer_email": "buyer@example.com",
  "created": 1790380525,
  "expires_at": 1790466925,
  "started_at": null,
  "metadata": { "user_id": "player_1042" }
}
```

A retry with the same `Idempotency-Key` and body answers `200 OK` with the same session.

## Retrieve a checkout session

`GET /v1/checkout_sessions/:id`

Returns a session of yours in the key's mode. Another account's session, or one in the other mode, is `404 not_found`. Reading a session past its `expires_at` marks it `expired`.

cURL:

```bash
curl https://pay.railbed.io/v1/checkout_sessions/pay_7AAiYH0Ykt11ED4hmfiN \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

Node.js:

```js
const session = await fetch(`https://pay.railbed.io/v1/checkout_sessions/${id}`, {
  headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` },
}).then((r) => r.json());
```

Python:

```python
session = requests.get(
    f"https://pay.railbed.io/v1/checkout_sessions/{id}",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    timeout=15,
).json()
```

PHP:

```php
<?php
$ch = curl_init(
  'https://pay.railbed.io/v1/checkout_sessions/' . rawurlencode($id)
);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
  ],
]);
$session = json_decode(curl_exec($ch), true);
```

The response is the [checkout session object](#the-checkout-session-object). To fulfil, read the [payment](https://railbed.io/docs/api/payments.md#retrieve-a-payment) instead: it adds the payment and settlement details.

## Start a checkout session

`POST /v1/checkout_sessions/:id/start`

For [your own checkout](https://railbed.io/docs/guides/custom-checkout.md): starts the session as the buyer would on the hosted page, and returns the card providers that can take it, each with a `handoff_url` to open in the buyer's browser. Starting assigns the payment's deposit address and locks the fee and the order's value in USD.

| Field | Type | Description |
|---|---|---|
| `customer_email` | string | The buyer's email. Optional when the session already has one; a new one replaces it |
| `country` | string | The buyer's two-letter country code (`US`, `DE`), any letter case. Optional. Use the buyer's country, never your server's |

Send `{}` when the saved email is enough.

cURL:

```bash
curl -X POST \
  https://pay.railbed.io/v1/checkout_sessions/pay_7AAiYH0Ykt11ED4hmfiN/start \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "country": "US" }'
```

Node.js:

```js
const url = `https://pay.railbed.io/v1/checkout_sessions/${id}/start`;
const started = await fetch(url, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ country: 'US' }),
}).then((r) => r.json());
```

Python:

```python
started = requests.post(
    f"https://pay.railbed.io/v1/checkout_sessions/{id}/start",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    json={"country": "US"},
    timeout=15,
).json()
```

PHP:

```php
<?php
$ch = curl_init(
  'https://pay.railbed.io/v1/checkout_sessions/' . rawurlencode($id) . '/start'
);
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['country' => 'US']),
]);
$started = json_decode(curl_exec($ch), true);
```

Response · response 200 OK:

```json
{
  "id": "pay_7AAiYH0Ykt11ED4hmfiN",
  "object": "checkout_session",
  "url": "https://pay.railbed.io/p/pay_7AAiYH0Ykt11ED4hmfiN",
  "status": "open",
  "livemode": false,
  "amount": "49.00",
  "currency": "USD",
  "description": "Pro Membership",
  "reference": "order_1042",
  "customer_email": "buyer@example.com",
  "created": 1790380525,
  "expires_at": 1790466925,
  "started_at": 1790380611,
  "metadata": { "user_id": "player_1042" },
  "country": "US",
  "providers": [
    {
      "id": "stripe",
      "name": "Stripe",
      "note": "Card, Apple Pay or Google Pay",
      "recommended": true,
      "handoff_url": "https://pay.railbed.io/go/pay_7AAiYH0Ykt11ED4hmfiN?provider=stripe"
    },
    {
      "id": "cashapp",
      "name": "Cash App",
      "note": "Cash App balance or card",
      "recommended": false,
      "handoff_url": "https://pay.railbed.io/go/pay_7AAiYH0Ykt11ED4hmfiN?provider=cashapp"
    }
  ]
}
```

The response is the session with two more fields:

| Field | Type | Description |
|---|---|---|
| `country` | string or null | The country you sent, uppercased |
| `providers` | array | The providers that can take this payment, best match first. Each has `id`, `name`, `note` (a short line on how the buyer pays), `recommended` (true for one) and `handoff_url` |

Starting again is safe: `started_at` and the deposit address stay the same. Providers depend on the amount, currency and country, so fetch them when the buyer reaches your checkout, and open a `handoff_url` only from the buyer's click, in a new tab.

| Status | Code | When |
|---|---|---|
| 400 | `invalid_email` | The session has no email and none was sent |
| 400 | `invalid_country` | `country` isn't two letters |
| 404 | `not_found` | Not a session of yours in this mode |
| 409 | `no_providers` | No provider can take this amount and currency now. Nothing was started |
| 409 | `already_paid`, `held`, `failed` | The payment has finished |
| 409 | `unavailable` | The account can't take payments now (Live: no payout wallet) |
| 410 | `expired`, `canceled` | The session can no longer be paid |
| 429 | `rate_limited` | More than about 12 starts a minute |
| 502 | `network_unavailable` | The card network didn't answer. Retry in a minute |
