API reference
The Railbed REST API. JSON over HTTPS, one secret key per server, idempotent creates, cursor pagination and plain error codes.
Base URL
https://pay.railbed.io/v1Every request uses HTTPS. The API has two resources: checkout sessions, which you create for each order, and payments, 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 |
GET /v1/checkout_sessions/:id | Retrieve a checkout session |
POST /v1/checkout_sessions/:id/start | Start a checkout session and get its card providers |
GET /v1/payments/:id | Retrieve a payment |
GET /v1/payments | List payments |
POST /v1/payments/:id/simulate | Simulate a payment (Test mode) |
Authentication
Send your secret key as a bearer token on every request. Keys are created and revoked in Developers, and each is shown once, when it's created.
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.
Requests and responses
- Send JSON bodies with
Content-Type: application/json, up to 64 KiB. Other types get415, larger bodies413, and bodies that aren't a JSON object400 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_receivedhas six). - Currencies are
USD,EUR,GBP,CADandAUD. - Timestamps in the API are Unix seconds. (A webhook event's
createdis Unix seconds too, but the payment inside it uses milliseconds; see event payloads.) - 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:
{
"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:
{
"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 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.