# 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.com/docs/api/checkout-sessions/ · Updated: 2026-09-27 · Railbed by DeepWork developer docs

For AI assistants: the index of every docs page, with the rules for an integration, is https://railbed.com/docs/llms.txt

## The checkout session object

| Field | Type | Description |
|---|---|---|
| `id` | string | The session's id, `pay_…`. The same id identifies its [payment](https://railbed.com/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.com/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 |
| `order_id` | string or null | The [order](https://railbed.com/docs/api/orders.md) this session is a payment attempt for, `ord_…`. Every session has one (`null` only on records from before orders existed) |

## 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.com/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 |
| `customer` | object or null | Who is buying: name, phone and your customer id. See [Orders and customers](#orders-and-customers) |
| `order` | object or null | What they're buying: items, discount, shipping, tax, addresses and your order id. See [Orders and customers](#orders-and-customers) |

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

Example: 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"
  }'
```

Example: 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();
```

Example: 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()
```

Example: 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);
```

Example: 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" },
  "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B"
}
```

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

### Orders and customers

Every session belongs to an [order](https://railbed.com/docs/api/orders.md). Send `order` and `customer` with the session, and the order has your items, totals, addresses and store order id, and the [customer](https://railbed.com/docs/api/customers.md) has the buyer's name and phone. They appear in the dashboard's **Orders** and **Customers** and in every [webhook](https://railbed.com/docs/webhooks/events.md#the-order-object). Without them, the order has one line, your `description`, for the whole amount.

The buyer's checkout page still shows your `description` and the `amount`. The order's details are for your records, not for the buyer to confirm.

Example: 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-attempt-1" \
  -d '{
    "amount": "135.31",
    "currency": "USD",
    "description": "Order #1042",
    "reference": "order_1042",
    "customer_email": "maya@example.com",
    "customer": {
      "name": "Maya Okafor",
      "phone": "+1 503 555 0142",
      "id": "88"
    },
    "order": {
      "id": "1042",
      "number": "1042",
      "url": "https://yourstore.com/admin/orders/1042",
      "store": {
        "id": "yourstore",
        "name": "yourstore.com",
        "url": "https://yourstore.com"
      },
      "items": [
        {
          "name": "Magnesium glycinate",
          "variant": "120 capsules",
          "sku": "HN-MG-120",
          "quantity": 2,
          "unit_amount": "32.00",
          "amount": "64.00"
        },
        {
          "name": "Daily greens",
          "sku": "HN-DG-30",
          "quantity": 1,
          "amount": "54.00"
        },
        { "name": "Shaker bottle", "quantity": 1, "amount": "12.00" }
      ],
      "discount": "13.00",
      "discount_code": "WELCOME10",
      "shipping": "8.95",
      "shipping_method": "Standard",
      "tax": "9.36",
      "shipping_address": {
        "name": "Maya Okafor",
        "line1": "418 Linden Avenue",
        "line2": "Apt 3B",
        "city": "Portland",
        "region": "OR",
        "postal_code": "97214",
        "country": "US"
      }
    },
    "success_url": "https://yourstore.com/orders/1042/thanks"
  }'
```

Example: 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-attempt-1',
  },
  body: JSON.stringify({
    amount: '135.31',
    currency: 'USD',
    description: 'Order #1042',
    reference: 'order_1042',
    customer_email: 'maya@example.com',
    customer: { name: 'Maya Okafor', phone: '+1 503 555 0142', id: '88' },
    order: {
      id: '1042',
      number: '1042',
      url: 'https://yourstore.com/admin/orders/1042',
      store: {
        id: 'yourstore',
        name: 'yourstore.com',
        url: 'https://yourstore.com',
      },
      items: [
        {
          name: 'Magnesium glycinate',
          variant: '120 capsules',
          sku: 'HN-MG-120',
          quantity: 2,
          unit_amount: '32.00',
          amount: '64.00',
        },
        { name: 'Daily greens', sku: 'HN-DG-30', quantity: 1, amount: '54.00' },
        { name: 'Shaker bottle', quantity: 1, amount: '12.00' },
      ],
      discount: '13.00',
      discount_code: 'WELCOME10',
      shipping: '8.95',
      shipping_method: 'Standard',
      tax: '9.36',
      shipping_address: {
        name: 'Maya Okafor',
        line1: '418 Linden Avenue',
        line2: 'Apt 3B',
        city: 'Portland',
        region: 'OR',
        postal_code: '97214',
        country: 'US',
      },
    },
    success_url: 'https://yourstore.com/orders/1042/thanks',
  }),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const session = await res.json();
```

Example: 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-attempt-1",
    },
    json={
        "amount": "135.31",
        "currency": "USD",
        "description": "Order #1042",
        "reference": "order_1042",
        "customer_email": "maya@example.com",
        "customer": {
            "name": "Maya Okafor",
            "phone": "+1 503 555 0142",
            "id": "88",
        },
        "order": {
            "id": "1042",
            "number": "1042",
            "url": "https://yourstore.com/admin/orders/1042",
            "store": {
                "id": "yourstore",
                "name": "yourstore.com",
                "url": "https://yourstore.com",
            },
            "items": [
                {
                    "name": "Magnesium glycinate",
                    "variant": "120 capsules",
                    "sku": "HN-MG-120",
                    "quantity": 2,
                    "unit_amount": "32.00",
                    "amount": "64.00",
                },
                {
                    "name": "Daily greens",
                    "sku": "HN-DG-30",
                    "quantity": 1,
                    "amount": "54.00",
                },
                {"name": "Shaker bottle", "quantity": 1, "amount": "12.00"},
            ],
            "discount": "13.00",
            "discount_code": "WELCOME10",
            "shipping": "8.95",
            "shipping_method": "Standard",
            "tax": "9.36",
            "shipping_address": {
                "name": "Maya Okafor",
                "line1": "418 Linden Avenue",
                "line2": "Apt 3B",
                "city": "Portland",
                "region": "OR",
                "postal_code": "97214",
                "country": "US",
            },
        },
        "success_url": "https://yourstore.com/orders/1042/thanks",
    },
    timeout=15,
)
r.raise_for_status()
session = r.json()
```

Example: 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-attempt-1',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => '135.31',
    'currency' => 'USD',
    'description' => 'Order #1042',
    'reference' => 'order_1042',
    'customer_email' => 'maya@example.com',
    'customer' => [
      'name' => 'Maya Okafor',
      'phone' => '+1 503 555 0142',
      'id' => '88',
    ],
    'order' => [
      'id' => '1042',
      'number' => '1042',
      'url' => 'https://yourstore.com/admin/orders/1042',
      'store' => [
        'id' => 'yourstore',
        'name' => 'yourstore.com',
        'url' => 'https://yourstore.com',
      ],
      'items' => [
        [
          'name' => 'Magnesium glycinate',
          'variant' => '120 capsules',
          'sku' => 'HN-MG-120',
          'quantity' => 2,
          'unit_amount' => '32.00',
          'amount' => '64.00',
        ],
        [
          'name' => 'Daily greens',
          'sku' => 'HN-DG-30',
          'quantity' => 1,
          'amount' => '54.00',
        ],
        ['name' => 'Shaker bottle', 'quantity' => 1, 'amount' => '12.00'],
      ],
      'discount' => '13.00',
      'discount_code' => 'WELCOME10',
      'shipping' => '8.95',
      'shipping_method' => 'Standard',
      'tax' => '9.36',
      'shipping_address' => [
        'name' => 'Maya Okafor',
        'line1' => '418 Linden Avenue',
        'line2' => 'Apt 3B',
        'city' => 'Portland',
        'region' => 'OR',
        'postal_code' => '97214',
        'country' => 'US',
      ],
    ],
    'success_url' => 'https://yourstore.com/orders/1042/thanks',
  ]),
]);
$session = json_decode(curl_exec($ch), true);
```

Example: Response · response 201 Created

```json
{
  "id": "pay_Rt5Wm2KxQ8zLb3NvYc7P",
  "object": "checkout_session",
  "url": "https://pay.railbed.io/p/pay_Rt5Wm2KxQ8zLb3NvYc7P",
  "status": "open",
  "livemode": true,
  "amount": "135.31",
  "currency": "USD",
  "description": "Order #1042",
  "reference": "order_1042",
  "customer_email": "maya@example.com",
  "created": 1790380525,
  "expires_at": 1790466925,
  "started_at": null,
  "metadata": null,
  "order_id": "ord_Vd3kR8mQ1xTn6LpZs0Wa"
}
```

[Retrieve the order](https://railbed.com/docs/api/orders.md#retrieve-an-order) with `order_id` to see what was saved.

### The customer field

| Field | Type | Description |
|---|---|---|
| `email` | string or null | The buyer's email, with the same rules as `customer_email`. Send either one; when you send both, they must be the same address |
| `name` | string or null | The buyer's name, up to 120 characters. Without it, the name on the shipping (or billing) address is used |
| `phone` | string or null | Up to 40 characters, in any format |
| `id` | string or null | The buyer's customer id in your store, up to 64 characters. It appears in the dashboard, on the order's `store.customer_id` and in the customer's [`store_accounts`](https://railbed.com/docs/api/customers.md) |

Name and phone reach the customer with the buyer's email: when the session is created with it, or once the buyer gives it and starts paying. See [how customers are made](https://railbed.com/docs/api/customers.md#how-customers-are-made).

### The order field

| Field | Type | Description |
|---|---|---|
| `id` | string or null | Your order id, up to 64 characters. A later session with the same `id` joins this order instead of making another. See [Store orders and retries](#store-orders-and-retries) |
| `number` | string or null | The order number your buyers see, up to 32 characters, such as `1042` (a leading `#` is dropped). Used only with `id` |
| `url` | string or null | The order's page in your admin, up to 1,000 characters. Used only with `id`, and only when it's an `https://` address |
| `store` | object or null | Which of your stores the order is from. Needs `id`: `store` without it is `400 invalid_order`. See below |
| `items` | array or null | What's being bought, 1–100 items. Leave it out for a single line. See below |
| `discount` | string or null | Taken off the items' total. Default `"0.00"`. It can be more than the items when store credit covers shipping or tax too, as long as the order still adds up to `amount` |
| `discount_code` | string or null | The code the buyer used, up to 64 characters |
| `shipping` | string or null | Shipping charged. Default `"0.00"` |
| `shipping_method` | string or null | Up to 120 characters, such as `Standard` |
| `tax` | string or null | Tax charged. Default `"0.00"` |
| `shipping_address` | object or null | Where to ship the order. See below |
| `billing_address` | object or null | The buyer's billing address |
| `fulfillment` | string or null | `none`, `unfulfilled` or `fulfilled`. Default `unfulfilled` when there's a shipping address, otherwise `none`. See [fulfilment](https://railbed.com/docs/api/orders.md#fulfilment) |

Money in `order` is written like `amount`: a decimal string with at most two decimal places, never a number. Zero is allowed. Text fields are trimmed, and a blank one counts as left out.

Each item in `items`:

| Field | Type | Description |
|---|---|---|
| `name` | string · required | Up to 200 characters |
| `quantity` | integer · required | A whole number from 1 to 100,000 |
| `amount` | string · required | The line's total for all its units, such as `"64.00"`. Zero is allowed |
| `unit_amount` | string or null | One unit's price, for display. It isn't checked against `amount` |
| `sku` | string or null | Up to 64 characters |
| `variant` | string or null | Which version, up to 120 characters, such as `120 capsules` |

`store` tells your stores apart, so two of them can use the same order ids. With one store, leave it out.

| Field | Type | Description |
|---|---|---|
| `id` | string · required | A stable id for the store, up to 64 characters. Keep it the same: it's how later sessions and [list filters](https://railbed.com/docs/api/orders.md#list-orders) find the order |
| `platform` | string | `woocommerce`, `shopify` or `other` (the default) |
| `name` | string or null | The store's name, up to 120 characters |
| `url` | string or null | The store's address, up to 1,000 characters. Kept only when it's `http://` or `https://` |

`shipping_address` and `billing_address` take the same fields, each optional:

| Field | Description |
|---|---|
| `name` | Up to 120 characters |
| `line1`, `line2` | Up to 200 characters each |
| `city` | Up to 120 characters |
| `region` | A state, province or county, up to 120 characters |
| `postal_code` | Up to 32 characters |
| `country` | A two-letter code such as `US`, in any letter case. Saved in capitals |

An address with nothing in it counts as none. Any field that breaks these rules is `400 invalid_order`, with `field` naming it, such as `order.items[2].quantity`. A `customer` that isn't an object, or a `customer.email` that differs from `customer_email`, is `400 invalid_customer`.

### Totals

With `items`, the parts must add up to `amount` exactly, to the cent: the items' `amount`s, less `discount`, plus `shipping` and `tax`. For the order above:

| Part | Amount |
|---|---|
| Magnesium glycinate × 2 | 64.00 |
| Daily greens | 54.00 |
| Shaker bottle | 12.00 |
| Discount, `WELCOME10` | −13.00 |
| Shipping | +8.95 |
| Tax | +9.36 |
| **`amount`** | **135.31** |

A session whose parts don't add up is `400 invalid_order_totals`, and nothing is created; the error's message shows the sum. Line amounts come before the order's discount: when your store discounts single lines, send either the discounted line totals or the whole discount in `discount`, not both.

Without `items`, the order has one line named after your `description`, for whatever makes the parts add up: `amount` + `discount` − `shipping` − `tax`. If that comes to less than zero, the session is `400 invalid_order_totals`.

### Store orders and retries

A session with `order.id` pays for an order in your store. Railbed keeps one order for each `order.id` (within its `store.id`, in each mode):

- **The first session creates the order.** Later sessions with the same `order.id` join it as more attempts at paying, for example after the buyer's card was declined or their session expired.
- **The latest session describes the order.** Its items, totals and addresses replace the order's, and so does its customer when it has an email. The order's `number` and fulfilment stay, and its timeline in the dashboard notes when the total or items changed.
- **A paid order refuses a new session.** When the order is `paid`, or `held` with money you could accept as paid or that part-paid it, a new session gets `409 order_paid` and nothing is created. Check the order before asking the buyer to pay again. An order held only for money you can't accept (it went somewhere other than your wallet) takes new sessions, so the buyer can pay.
- **Earlier sessions stay open.** Joining doesn't close them, and one that's still open can still be paid. Send the buyer only to the newest session's `url`. A buyer who pays twice leaves two paid payments in the order's `payment_ids`; refund one from your wallet.
- **Use a new `Idempotency-Key` for each attempt.** Repeating a request with the same key and body returns its original session, even after the order is paid: a replay is never refused with `order_paid`. The key covers `order` and `customer` too, so the same key with a different order is `409 idempotency_conflict`.

Without `order.id`, every session creates a new order.

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

Example: cURL

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

Example: 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());
```

Example: 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()
```

Example: 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.com/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.com/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.

Example: 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" }'
```

Example: 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());
```

Example: 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()
```

Example: 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);
```

Example: 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" },
  "order_id": "ord_M4tQx8ZkP1vRn6WcLs2B",
  "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 |
