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

Base URL
https://pay.railbed.io/v1

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

EndpointWhat it does
POST /v1/checkout_sessionsCreate a checkout session
GET /v1/checkout_sessions/:idRetrieve a checkout session
POST /v1/checkout_sessions/:id/startStart a checkout session and get its card providers
GET /v1/payments/:idRetrieve a payment
GET /v1/paymentsList payments
POST /v1/payments/:id/simulateSimulate a payment (Test mode)
GET /v1/orders/:idRetrieve an order
GET /v1/ordersList orders, or find one by your store's order id
PATCH /v1/orders/:idUpdate an order's fulfilment
POST /v1/orders/:id/eventsReport a cancellation or refund on the order's timeline
GET /v1/customers/:idRetrieve a customer
GET /v1/customersList 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.

An authenticated request
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 levelScopesWhat the key can do
Full access (the default)All five scopesUse every endpoint
Read onlypayments:read, orders:read, customers:readRetrieve 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.

An agent's request
curl "https://pay.railbed.io/v1/payments?limit=1" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
ResponseCauseWhat the agent should do
401 invalid_tokenThe token expired, is malformed or wasn't issued for RailbedGet a new access token
401 agent_disconnectedThe agent was disconnected in the dashboard, or the person who connected it no longer manages keys for the businessAsk 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.

ScopeEndpoints
payments:readGET /v1/payments, GET /v1/payments/:id, GET /v1/checkout_sessions/:id
checkouts:writePOST /v1/checkout_sessions, POST /v1/checkout_sessions/:id/start, POST /v1/payments/:id/simulate
orders:readGET /v1/orders, GET /v1/orders/:id
orders:writePATCH /v1/orders/:id, POST /v1/orders/:id/events
customers:readGET /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):

A request without a credential
WWW-Authenticate: Bearer resource_metadata="https://pay.railbed.io/.well-known/oauth-protected-resource/v1"
401 Unauthorized

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.

The protected resource metadata
{
  "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/"
}
200 OK

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

SituationResponse
First request with a key201 and the new session
Same key, same body (a retry)200 and the same session, with its current status
Same key, different body409 idempotency_conflict. Use a new key for a different order
Same key and body while the first request is still runningThe 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 characters400 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:

A page
{
  "data": [{ "id": "pay_…", "object": "payment", "status": "paid" }],
  "has_more": true,
  "next_cursor": "pay_Q3cNtwPT0bRqGmS5eZkB"
}
200 OK

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:

An error
{
  "error": {
    "code": "invalid_amount",
    "message": "amount must be a decimal string between \"1.00\" and \"100000.00\".",
    "field": "amount"
  }
}
400 Bad Request

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.

Updated · This page as Markdown