# API reference

> The Railbed REST API. JSON over HTTPS, one secret key per server, idempotent creates, cursor pagination and plain error codes.

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

## Base URL

Base URL:

```text
https://pay.railbed.io/v1
```

Every request uses HTTPS. The API has two resources: [checkout sessions](https://railbed.io/docs/api/checkout-sessions.md), which you create for each order, and [payments](https://railbed.io/docs/api/payments.md), which you read to fulfil them. A session and its payment share one id (`pay_…`).

| Endpoint | What it does |
|---|---|
| `POST /v1/checkout_sessions` | [Create a checkout session](https://railbed.io/docs/api/checkout-sessions.md#create-a-checkout-session) |
| `GET /v1/checkout_sessions/:id` | [Retrieve a checkout session](https://railbed.io/docs/api/checkout-sessions.md#retrieve-a-checkout-session) |
| `POST /v1/checkout_sessions/:id/start` | [Start a checkout session](https://railbed.io/docs/api/checkout-sessions.md#start-a-checkout-session) and get its card providers |
| `GET /v1/payments/:id` | [Retrieve a payment](https://railbed.io/docs/api/payments.md#retrieve-a-payment) |
| `GET /v1/payments` | [List payments](https://railbed.io/docs/api/payments.md#list-payments) |
| `POST /v1/payments/:id/simulate` | [Simulate a payment](https://railbed.io/docs/api/payments.md#simulate-a-payment) (Test mode) |

## Authentication

Send your secret key as a bearer token on every request. Keys are created and revoked in [Developers](https://app.railbed.io/developers), and each is shown once, when it's created.

An authenticated request:

```bash
curl "https://pay.railbed.io/v1/payments?limit=1" \
  -H "Authorization: Bearer rb_test_…"
```

The key decides everything about the request: which account it belongs to and whether it works on Test or Live payments. `rb_test_…` keys see only Test data and `rb_live_…` keys only Live data; nothing else can switch the mode. A missing, malformed or revoked key gets `401 invalid_api_key`; a suspended account gets `403 suspended`.

> [!IMPORTANT]
> Secret keys belong on your server. The API doesn't accept requests from browsers (it sends no CORS headers), and there's no publishable key: your web pages and apps call your server, and your server calls Railbed.

## Requests and responses

- Send JSON bodies with `Content-Type: application/json`, up to 64 KiB. Other types get `415`, larger bodies `413`, and bodies that aren't a JSON object `400 invalid_json`.
- Responses are JSON and are never cached (`Cache-Control: no-store`).
- Unknown fields in a request are ignored. Build against the fields documented here.
- **Money** is a decimal string, never a number, so no amount is ever rounded in transit. Prices have two places (`"49.00"`); settlement amounts in the coin can have more (`merchant_received` has six).
- **Currencies** are `USD`, `EUR`, `GBP`, `CAD` and `AUD`.
- **Timestamps** in the API are Unix seconds. (A webhook event's `created` is Unix seconds too, but the payment inside it uses milliseconds; see [event payloads](https://railbed.io/docs/webhooks/events.md#the-payment-object).)
- **Ids** are prefixed: `pay_` for sessions and payments, `evt_` for webhook events.

## Idempotency

Networks fail. To retry a create safely, send an `Idempotency-Key` header: a value unique to the order attempt, 1–120 printable ASCII characters. Save it with your order before the first call.

| Situation | Response |
|---|---|
| First request with a key | `201` and the new session |
| Same key, same body (a retry) | `200` and the same session, with its current status |
| Same key, different body | `409 idempotency_conflict`. Use a new key for a different order |
| Same key and body while the first request is still running | The same session: one request gets `201`, the others `200`. Rarely, `409 idempotency_conflict` asks you to retry in a moment |
| Key longer than 120 characters, blank, or with other characters | `400 invalid_idempotency_key` (never truncated) |

Keys are scoped to your account and the key's mode, and remembered for 24 hours; after that the same key creates a new session. Metadata key order doesn't matter when comparing bodies. Without the header, every create makes a new session, and your `reference` alone doesn't prevent duplicates.

## Pagination

`GET /v1/payments` returns the newest payments first, up to `limit` (1–100, default 20) at a time:

A page · response 200 OK:

```json
{
  "data": [{ "id": "pay_…", "object": "payment", "status": "paid" }],
  "has_more": true,
  "next_cursor": "pay_Q3cNtwPT0bRqGmS5eZkB"
}
```

Pass `next_cursor` as `starting_after` to get the next page, until `next_cursor` is `null`. The order is stable, even for payments created in the same second. A `limit` outside 1–100, or a cursor that isn't one of your payments in this mode, gets `400` rather than a silent first page.

## Rate limits

Each account can make about **120 requests a minute** across all its keys and modes, and **12 session starts a minute**. Over the limit, requests get `429 rate_limited` with a `Retry-After: 60` header. Queue requests on your server and back off with a little randomness; never make a request per animation frame or per page view.

The limits protect the service rather than set a quota: they're enforced per region and are approximate, so plan well below them.

## Errors

Errors use HTTP status codes and a JSON body with a stable `code`, a sentence for people, and sometimes the `field` at fault:

An error · response 400 Bad Request:

```json
{
  "error": {
    "code": "invalid_amount",
    "message": "amount must be a decimal string between \"1.00\" and \"100000.00\".",
    "field": "amount"
  }
}
```

Branch on `code` and the status, never on `message`, which may be reworded. [Errors](https://railbed.io/docs/api/errors.md) lists every code.

## Versioning

The API is `v1`. Changes within `v1` only add things: new fields in responses, new optional request fields, new error codes, new webhook event types. Write your integration to ignore fields and event types it doesn't know. A change that could break an integration would come as a new version.
