API reference
The Railbed REST API. JSON over HTTPS, secret keys for your servers and access tokens for connected agents, scopes, idempotent creates, cursor pagination and plain error codes.
On this page
Base URL
https://pay.railbed.io/v1Every request uses HTTPS. You create checkout sessions, one for each attempt at paying, and read their payments to fulfil orders. A session and its payment share one id (pay_…). Each session belongs to an order, which holds what was bought and where it ships, and each order to a customer.
| 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) |
GET /v1/orders/:id | Retrieve an order |
GET /v1/orders | List orders, or find one by your store's order id |
PATCH /v1/orders/:id | Update an order's fulfilment |
POST /v1/orders/:id/events | Report a cancellation or refund on the order's timeline |
GET /v1/customers/:id | Retrieve a customer |
GET /v1/customers | List customers, or find one by email |
Authentication
Send a bearer token on every request: a secret key from your server, or the access token of an AI agent you've connected to your account. The API treats both the same way.
curl "https://pay.railbed.io/v1/payments?limit=1" \
-H "Authorization: Bearer rb_test_…"The credential decides everything about the request: which account it belongs to, whether it works on Test or Live data, and which endpoints it may use. Nothing else can switch the mode. A suspended account gets 403 suspended.
Secret keys
Keys are created and revoked in Developers, and each is shown once, when it's created. rb_test_… keys see only Test data and rb_live_… keys only Live data. A missing, malformed or revoked key gets 401 invalid_api_key.
Each key has an access level, chosen when you create it:
| Access level | Scopes | What the key can do |
|---|---|---|
| Full access (the default) | All five scopes | Use every endpoint |
| Read only | payments:read, orders:read, customers:read | Retrieve and list payments, checkout sessions, orders and customers. Nothing that creates or changes |
Keys created before access levels existed have full access. WooCommerce keys always do, because the plugin creates checkout sessions.
Agent access tokens
An AI agent gets access through agent sign-in. It reads railbed.com/auth.md, a person who can manage keys approves it in the dashboard, and the agent receives its own access tokens. Each lasts about five minutes, and the agent gets the next one itself from the token endpoint that auth.md describes. A token carries the business, the mode and the access level (Full access or Read only) the person chose.
curl "https://pay.railbed.io/v1/payments?limit=1" \
-H "Authorization: Bearer $ACCESS_TOKEN"| Response | Cause | What the agent should do |
|---|---|---|
401 invalid_token | The token expired, is malformed or wasn't issued for Railbed | Get a new access token |
401 agent_disconnected | The agent was disconnected in the dashboard, or the person who connected it no longer manages keys for the business | Ask the person to connect it again |
Scopes
Every endpoint needs one scope. A key or agent whose access level doesn't include it gets 403 insufficient_scope, and the message names the scope.
| Scope | Endpoints |
|---|---|
payments:read | GET /v1/payments, GET /v1/payments/:id, GET /v1/checkout_sessions/:id |
checkouts:write | POST /v1/checkout_sessions, POST /v1/checkout_sessions/:id/start, POST /v1/payments/:id/simulate |
orders:read | GET /v1/orders, GET /v1/orders/:id |
orders:write | PATCH /v1/orders/:id, POST /v1/orders/:id/events |
customers:read | GET /v1/customers, GET /v1/customers/:id |
Discovery
Every 401 from the API tells an agent where to find out how to get access, in a WWW-Authenticate header (RFC 9728):
WWW-Authenticate: Bearer resource_metadata="https://pay.railbed.io/.well-known/oauth-protected-resource/v1"When a credential was sent and refused, the header adds error="invalid_token". A 403 insufficient_scope carries error="insufficient_scope", scope="…", naming the scope the endpoint needs.
The protected resource metadata names the API, its authorization server, the scopes and these docs. The same document is also at https://pay.railbed.io/.well-known/oauth-protected-resource. An agent starting out should read auth.md, which takes it through the rest.
{
"resource": "https://pay.railbed.io/v1",
"resource_name": "Railbed API",
"authorization_servers": ["https://astounding-resonance-81.authkit.app"],
"scopes_supported": ["payments:read", "checkouts:write", "orders:read", "orders:write", "customers:read"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://railbed.com/docs/api/"
}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, order and customer inside it use milliseconds; see event payloads.) - Ids are prefixed:
pay_for sessions and payments,ord_for orders,cus_for customers,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. Bodies are compared including order and customer, and metadata key order doesn't matter. Without the header, every create makes a new session, and your reference alone doesn't prevent duplicates.
Pagination
The list endpoints (GET /v1/payments, GET /v1/orders and GET /v1/customers) return the newest 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 objects created in the same second. A limit outside 1–100, or a cursor that isn't an object from the same list 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. Each connected AI agent has its own 120 a minute, so an agent can't use up your servers' budget; its live session starts still count toward the account's 12. Requests with a missing or invalid secret key are limited to about 60 a minute from one address. 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.