# Hosted checkout

> Create a checkout session for each order on your server, send the buyer to Railbed's hosted page, and bring them back to your store once they've paid.

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

## How it works

Your server creates a [checkout session](https://railbed.io/docs/api/checkout-sessions.md) for the order and redirects the buyer to its `url`. Railbed's page asks for the buyer's email, shows the card providers that can take this payment in their country (the recommended one first), and opens the chosen provider in a new tab. The checkout tab waits there while the buyer pays, then sends them to your `success_url`.

1. **Order placed.** Your server saves the order, then calls `POST /v1/checkout_sessions`.
2. **Redirect.** Your server sends the buyer to the session's `url`.
3. **Payment.** The buyer pays on the provider's page, in its own tab.
4. **Return.** The Railbed tab sees the payment confirmed and sends the buyer to your `success_url`.
5. **Fulfilment.** Your server fulfils the order from the `payment.paid` webhook or `GET /v1/payments/:id`, never from the return alone.

## Create the session

Create one session per order attempt, with an `Idempotency-Key` saved alongside the order before you call. A retry after a timeout then returns the same session instead of creating a second one.

Node.js:

```js
// POST /checkout on your server, after saving the order
app.post('/checkout', async (req, res) => {
  const order = await orders.create({
    userId: req.user.id,
    sku: 'pro-monthly',
    price: '49.00',
    currency: 'USD',
  });

  const response = 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_${order.id}`,
    },
    body: JSON.stringify({
      amount: order.price,
      currency: order.currency,
      description: 'Pro Membership · monthly',
      reference: `order_${order.id}`,
      customer_email: req.user.email,
      metadata: { user_id: String(req.user.id) },
      success_url:
        `https://yourstore.com/orders/${order.id}/thanks?payment={PAYMENT_ID}`,
      cancel_url: `https://yourstore.com/cart`,
    }),
  });
  if (!response.ok) {
    return res.status(502).send(
      'Checkout is unavailable. Try again in a moment.',
    );
  }
  const session = await response.json();

  await orders.update(order.id, { paymentId: session.id });
  res.redirect(303, session.url);
});
```

Python:

```python
# Flask: after saving the order
@app.post("/checkout")
def checkout():
    order = orders.create(
        user_id=current_user.id,
        sku="pro-monthly",
        price="49.00",
        currency="USD",
    )
    r = requests.post(
        "https://pay.railbed.io/v1/checkout_sessions",
        headers={
            "Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}",
            "Idempotency-Key": f"order_{order.id}",
        },
        json={
            "amount": order.price,
            "currency": order.currency,
            "description": "Pro Membership · monthly",
            "reference": f"order_{order.id}",
            "customer_email": current_user.email,
            "metadata": {"user_id": str(current_user.id)},
            "success_url": (
                f"https://yourstore.com/orders/{order.id}"
                f"/thanks?payment={{PAYMENT_ID}}"
            ),
            "cancel_url": "https://yourstore.com/cart",
        },
        timeout=15,
    )
    if not r.ok:
        return "Checkout is unavailable. Try again in a moment.", 502
    session = r.json()
    orders.update(order.id, payment_id=session["id"])
    return redirect(session["url"], code=303)
```

PHP:

```php
<?php
// After saving the order
$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_' . $order['id'],
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => $order['price'],
    'currency' => $order['currency'],
    'description' => 'Pro Membership · monthly',
    'reference' => 'order_' . $order['id'],
    'customer_email' => $user['email'],
    'metadata' => ['user_id' => (string) $user['id']],
    'success_url' => 'https://yourstore.com/orders/' . $order['id']
      . '/thanks?payment={PAYMENT_ID}',
    'cancel_url' => 'https://yourstore.com/cart',
  ]),
]);
$session = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 300) {
  http_response_code(502);
  exit('Checkout is unavailable.');
}
save_payment_id($order['id'], $session['id']);
header('Location: ' . $session['url'], true, 303);
```

> [!NOTE]
> In Python f-strings, `{{PAYMENT_ID}}` writes the literal `{PAYMENT_ID}` placeholder. Railbed fills it in, not your code.

## Return URLs

| Field | When the buyer sees it |
|---|---|
| `success_url` | Once the payment is confirmed, the Railbed checkout tab sends the buyer here. `{PAYMENT_ID}` and `{REFERENCE}` in the address are replaced with the payment's id and your reference |
| `cancel_url` | A link back to your store, named after your business, while the buyer is paying and after a decline or an expiry |

Both are optional, must be full `https://` addresses in Live mode (Test also accepts `http://`), up to 1,000 characters, and can't contain a username or password. Without a `success_url`, the buyer sees Railbed's own confirmation.

> [!IMPORTANT]
> A buyer can open a `success_url` without paying, and a buyer who pays can close the tab before it loads. Treat the return page as a status screen: show "Payment confirmed" only after your server has seen the payment as `paid`, and fulfil from your server. See [Fulfil orders safely](https://railbed.io/docs/guides/fulfilment.md).

## What the buyer sees

- **The order.** Your business name and logo, the description and the price, in the currency you set.
- **Their email.** Prefilled when you send `customer_email`. Providers use it for their receipt.
- **Card providers.** Only those that serve the buyer's country, take the currency and accept the amount, the recommended one first. The buyer pays on the provider's page.
- **Waiting.** The checkout tab says to finish paying in the provider's tab and updates by itself. If the browser blocks the new tab, the provider opens in the same tab instead.
- **The result.** Paid, being reviewed (held), declined or expired, each with a clear next step.

A session can be paid for 24 hours. After that it's `expired`; create a new one if the buyer comes back.

## When there's no provider

Providers have minimum amounts and regional limits. When none can take a payment, the checkout says so and asks the buyer to try later. You can check in advance by starting the session through the API: `POST /v1/checkout_sessions/:id/start` answers `409 no_providers`. Orders of a few dollars are the most likely to hit provider minimums.
