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

Source: https://railbed.com/docs/api/ · Updated: 2026-10-05 · Railbed by DeepWork developer docs

For AI assistants: the index of every docs page, with the rules for an integration, is https://railbed.com/docs/llms.txt

## Base URL

Example: Base URL

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

Every request uses HTTPS. You create [checkout sessions](https://railbed.com/docs/api/checkout-sessions.md), one for each attempt at paying, and read their [payments](https://railbed.com/docs/api/payments.md) to fulfil orders. A session and its payment share one id (`pay_…`). Each session belongs to an [order](https://railbed.com/docs/api/orders.md), which holds what was bought and where it ships, and each order to a [customer](https://railbed.com/docs/api/customers.md).

| Endpoint | What it does |
|---|---|
| `POST /v1/checkout_sessions` | [Create a checkout session](https://railbed.com/docs/api/checkout-sessions.md#create-a-checkout-session) |
| `GET /v1/checkout_sessions/:id` | [Retrieve a checkout session](https://railbed.com/docs/api/checkout-sessions.md#retrieve-a-checkout-session) |
| `POST /v1/checkout_sessions/:id/start` | [Start a checkout session](https://railbed.com/docs/api/checkout-sessions.md#start-a-checkout-session) and get its card providers |
| `GET /v1/payments/:id` | [Retrieve a payment](https://railbed.com/docs/api/payments.md#retrieve-a-payment) |
| `GET /v1/payments` | [List payments](https://railbed.com/docs/api/payments.md#list-payments) |
| `POST /v1/payments/:id/simulate` | [Simulate a payment](https://railbed.com/docs/api/payments.md#simulate-a-payment) (Test mode) |
| `GET /v1/orders/:id` | [Retrieve an order](https://railbed.com/docs/api/orders.md#retrieve-an-order) |
| `GET /v1/orders` | [List orders](https://railbed.com/docs/api/orders.md#list-orders), or find one by your store's order id |
| `PATCH /v1/orders/:id` | [Update an order](https://railbed.com/docs/api/orders.md#update-an-order)'s fulfilment |
| `POST /v1/orders/:id/events` | [Report a cancellation or refund](https://railbed.com/docs/api/orders.md#report-a-cancellation-or-refund) on the order's timeline |
| `GET /v1/customers/:id` | [Retrieve a customer](https://railbed.com/docs/api/customers.md#retrieve-a-customer) |
| `GET /v1/customers` | [List customers](https://railbed.com/docs/api/customers.md#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](https://railbed.com/docs/agents.md#connect-an-agent). The API treats both the same way.

Example: An authenticated request

```bash
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](https://app.railbed.io/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](#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.

> [!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.

### Agent access tokens

An AI agent gets access through [agent sign-in](https://railbed.com/docs/agents.md#connect-an-agent). It reads [railbed.com/auth.md](https://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.

Example: An agent's request

```bash
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](https://www.rfc-editor.org/rfc/rfc9728)):

Example: A request without a credential · response 401 Unauthorized

```http
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](https://railbed.com/auth.md), which takes it through the rest.

Example: The protected resource metadata · response 200 OK

```json
{
  "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 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](https://railbed.com/docs/webhooks/events.md#the-payment-object).)
- **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:

Example: 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 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:

Example: 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.com/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.
