# Orders

> An order is what a buyer tried to buy, with its items, totals, addresses and customer. Every checkout session belongs to one. Read orders to fulfil them, and report fulfilment back when you ship.

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

## The order object

| Field | Type | Description |
|---|---|---|
| `id` | string | The order's id, `ord_…` |
| `object` | string | `"order"` |
| `livemode` | boolean | `false` for Test mode, `true` for Live mode |
| `number` | integer | Railbed's order number, counting from 1001 in each mode. Your store's own number is `store.order_number` |
| `status` | string | `open`, `pending`, `paid`, `held`, `failed`, `expired` or `canceled`, from its payments. See [Status](#status) |
| `needs_review` | boolean | `true` when it's `held` and waits on you: a held payment you could accept as paid, or paid payments that don't cover `total`. Such an order refuses new sessions with `409 order_paid` |
| `fulfillment` | string | `none`, `unfulfilled` or `fulfilled`. See [Fulfilment](#fulfilment) |
| `fulfilled_at` | integer or null | When it was marked `fulfilled`, in Unix seconds |
| `customer_id` | string or null | Its [customer](https://railbed.io/docs/api/customers.md), `cus_…`. Null until the buyer has given an email |
| `currency` | string | The currency of every amount on the order |
| `subtotal` | string | The items' `amount`s added up |
| `discount` | string | Taken off the subtotal, and larger than it only when store credit also covered shipping or tax. `"0.00"` when there's none |
| `discount_code` | string or null | The code or codes the buyer used |
| `shipping` | string | Shipping charged |
| `shipping_method` | string or null | How it ships, such as `Standard` |
| `tax` | string | Tax charged |
| `total` | string | `subtotal` − `discount` + `shipping` + `tax`: the latest session's `amount` |
| `amount_paid` | string | What its paid payments in `currency` add up to. `"0.00"` until one is paid |
| `items` | array | What was bought, in the order you sent it. See [Items](#items) |
| `shipping_address` | object or null | Where to send it. See [Addresses](#addresses) |
| `billing_address` | object or null | The buyer's billing address |
| `store` | object or null | Where the order lives in your systems, for orders sent with `order.id`. See [Store](#store) |
| `payment_ids` | array | Every payment attempt at this order, newest first (`pay_…`) |
| `created` | integer | When the order was created, in Unix seconds |
| `paid_at` | integer or null | When its first payment became `paid`, in Unix seconds |

Amounts are decimal strings, like every price in the API. They're what the buyer was charged in `currency`, not what arrived in your wallet: that's on each [payment](https://railbed.io/docs/api/payments.md#the-payment-object).

Example: An order from a store · response 200 OK

```json
{
  "id": "ord_Vd3kR8mQ1xTn6LpZs0Wa",
  "object": "order",
  "livemode": true,
  "number": 1187,
  "status": "paid",
  "needs_review": false,
  "fulfillment": "unfulfilled",
  "fulfilled_at": null,
  "customer_id": "cus_Hc7nQ2wVz9KpT4mRb1Ye",
  "currency": "USD",
  "subtotal": "130.00",
  "discount": "13.00",
  "discount_code": "WELCOME10",
  "shipping": "8.95",
  "shipping_method": "Standard",
  "tax": "9.36",
  "total": "135.31",
  "amount_paid": "135.31",
  "items": [
    {
      "name": "Magnesium glycinate",
      "variant": "120 capsules",
      "sku": "HN-MG-120",
      "quantity": 2,
      "unit_amount": "32.00",
      "amount": "64.00"
    },
    {
      "name": "Daily greens",
      "variant": null,
      "sku": "HN-DG-30",
      "quantity": 1,
      "unit_amount": null,
      "amount": "54.00"
    },
    {
      "name": "Shaker bottle",
      "variant": null,
      "sku": null,
      "quantity": 1,
      "unit_amount": null,
      "amount": "12.00"
    }
  ],
  "shipping_address": {
    "name": "Maya Okafor",
    "line1": "418 Linden Avenue",
    "line2": "Apt 3B",
    "city": "Portland",
    "region": "OR",
    "postal_code": "97214",
    "country": "US"
  },
  "billing_address": null,
  "store": {
    "platform": "other",
    "id": "yourstore",
    "name": "yourstore.com",
    "url": "https://yourstore.com",
    "order_id": "1042",
    "order_number": "1042",
    "order_url": "https://yourstore.com/admin/orders/1042",
    "customer_id": "88"
  },
  "payment_ids": ["pay_Rt5Wm2KxQ8zLb3NvYc7P"],
  "created": 1790380525,
  "paid_at": 1790381342
}
```

### Items

| Field | Type | Description |
|---|---|---|
| `name` | string | What the line is |
| `variant` | string or null | Which version, such as `120 capsules` or `Blue, L` |
| `sku` | string or null | Your stock-keeping code |
| `quantity` | integer | How many units |
| `unit_amount` | string or null | One unit's price, when it was sent. For display only |
| `amount` | string | The line's total. The order's totals add up from these |

An order made without `order.items` has one line, named after the session's `description`, with quantity 1. Its amount is whatever makes the totals add up: `amount` + `discount` − `shipping` − `tax`, which is the whole amount when you send no totals.

### Addresses

`shipping_address` and `billing_address` have the same fields, each a string or null: `name`, `line1`, `line2`, `city`, `region` (a state, province or county), `postal_code` and `country` (a two-letter code such as `US`). They're stored as sent, not checked against a postal service.

### Store

For orders you sent with `order.id`, `store` says where the order lives, so you can find it again and link back to it:

| Field | Type | Description |
|---|---|---|
| `platform` | string | `woocommerce`, `shopify` or `other` |
| `id` | string | The `order.store.id` you sent. Empty (`""`) when you sent no `store` |
| `name` | string or null | The store's name |
| `url` | string or null | The store's address |
| `order_id` | string | Your order id, as sent in `order.id` |
| `order_number` | string or null | The number your buyers see |
| `order_url` | string or null | The order's page in your admin |
| `customer_id` | string or null | The buyer's id in your store: the `customer.id` you sent |

`store` is null for orders sent without `order.id`, and for orders from Railbed's own checkouts and payment links.

## How orders are made

Every checkout session belongs to an order, and its `order_id` says which. The order is created in the same step as the session, so a session never exists without one.

- **From the API.** Send [`order` and `customer`](https://railbed.io/docs/api/checkout-sessions.md#orders-and-customers) when you create a session, and the order has your items, totals and addresses. Without them, the order has one line, the session's `description`, for the whole amount.
- **From your store's order id.** A session sent with `order.id` joins the order that already has that id, if there is one. That's how a buyer's second try at paying the same order stays one order. See [Store orders and retries](https://railbed.io/docs/api/checkout-sessions.md#store-orders-and-retries).
- **From Railbed's checkouts and payment links.** Checkout pages, pricing tables, buy buttons and payment links make orders too, with one item: the product, plan or what the link is for. A buyer who tries the same checkout again within a day, with the same email, price and plan, joins their unpaid order.

Each payment is one attempt at paying for its order. An order can have several, in `payment_ids`, and more than one of them can be paid if the buyer paid twice.

The API reads orders and updates their fulfilment. It doesn't create orders on their own, edit their items or delete them: an order's contents change only when a new session for the same `order.id` arrives.

## Status

An order's `status` comes from its payments, and changes in the same step as theirs:

1. `paid` when its paid payments cover `total` (they add up to at least `total` in the order's `currency`).
2. Otherwise `held` when any of its payments is held for review, or is paid but doesn't cover `total` (a part-payment after the order grew, or one in another currency).
3. Otherwise the latest payment's status: `open`, `pending`, `failed` or `expired`, or `canceled` when it's a payment link you canceled.

| Status | Meaning | What to do |
|---|---|---|
| `open` | Created, or started but not yet handed to a provider | Wait |
| `pending` | The buyer went to a provider to pay | Wait |
| `paid` | Payments for it arrived, passed Railbed's checks and cover `total` | Fulfil it, once |
| `held` | Money arrived but failed a check, or what's paid doesn't cover `total` (`amount_paid`) | Don't fulfil. Review it in the dashboard, and check with the buyer |
| `failed` | The latest attempt was declined (Test mode) | Let the buyer try again |
| `expired` | The latest attempt expired unpaid | Treat as abandoned; a late payment can still make it `paid` |
| `canceled` | You canceled its payment link | Nothing to do |

The status can't be set through the API. Like a payment, an order can become `paid` after `expired` when money arrives late, and a `held` order becomes `paid` when you accept the payment. A `paid` order stays `paid`.

## Fulfilment

`fulfillment` is your record of whether the order has been delivered. It doesn't affect the payment.

| Value | Meaning |
|---|---|
| `none` | Nothing to ship: digital goods, memberships, services. The default without a shipping address |
| `unfulfilled` | Waiting to be shipped or delivered. The default when the order has a shipping address |
| `fulfilled` | Delivered or on its way. `fulfilled_at` says when it was marked |

You can set the starting value with `order.fulfillment` when you create the session. After that:

- **Orders sent with `order.id`** get their fulfilment from your store, through [Update an order](#update-an-order). The dashboard shows it but doesn't change it, so the two never disagree. The [WooCommerce plugin](https://railbed.io/docs/guides/woocommerce.md#orders-customers-and-fulfilment) does this for you.
- **Other orders** can be marked fulfilled in the dashboard's **Orders** once they're paid, or through the API.

## Retrieve an order

`GET /v1/orders/:id`

Returns an order of yours in the key's mode. Another account's order, or one in the other mode, is `404 not_found`.

Example: cURL

```bash
curl https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

Example: Node\.js

```js
const order = await fetch(`https://pay.railbed.io/v1/orders/${id}`, {
  headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` },
}).then((r) => r.json());
```

Example: Python

```python
order = requests.get(
    f"https://pay.railbed.io/v1/orders/{id}",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    timeout=15,
).json()
```

Example: PHP

```php
<?php
$ch = curl_init('https://pay.railbed.io/v1/orders/' . rawurlencode($id));
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
  ],
]);
$order = json_decode(curl_exec($ch), true);
```

The response is the [order object](#the-order-object). To find the order behind a payment or webhook, use the payment's `order_id` (`orderId` in webhooks); webhooks also carry the [order itself](https://railbed.io/docs/webhooks/events.md#the-order-object).

## List orders

`GET /v1/orders`

Returns your orders in the key's mode, newest first by creation. Filter by customer, or find the order you sent with a given `order.id`.

| Parameter | Type | Description |
|---|---|---|
| `limit` | integer | 1–100. Default 20 |
| `starting_after` | string | An order id from the previous page's `next_cursor` |
| `customer_id` | string | Only this customer's orders |
| `store_order_id` | string | Only the order you sent with this `order.id` |
| `store_id` | string | Only orders sent with this `order.store.id`. With `store_order_id`, the one order you sent with both; leave it out for orders sent without `store` |

Example: cURL

```bash
curl "https://pay.railbed.io/v1/orders?store_id=yourstore&store_order_id=1042" \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY"
```

Example: Node\.js

```js
const url = new URL('https://pay.railbed.io/v1/orders');
url.searchParams.set('store_id', 'yourstore');
url.searchParams.set('store_order_id', '1042');
const page = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` },
}).then((r) => r.json());
const order = page.data[0] ?? null;
```

Example: Python

```python
page = requests.get(
    "https://pay.railbed.io/v1/orders",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    params={"store_id": "yourstore", "store_order_id": "1042"},
    timeout=15,
).json()
order = page["data"][0] if page["data"] else None
```

Example: PHP

```php
<?php
$query = http_build_query([
  'store_id' => 'yourstore',
  'store_order_id' => '1042',
]);
$ch = curl_init('https://pay.railbed.io/v1/orders?' . $query);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
  ],
]);
$page = json_decode(curl_exec($ch), true);
$order = $page['data'][0] ?? null;
```

Example: Response (trimmed) · response 200 OK

```json
{
  "data": [
    {
      "id": "ord_Vd3kR8mQ1xTn6LpZs0Wa",
      "object": "order",
      "number": 1187,
      "status": "paid",
      "fulfillment": "unfulfilled",
      "total": "135.31"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Each item is a full [order object](#the-order-object) (trimmed here). Page through with `next_cursor` as on [List payments](https://railbed.io/docs/api/payments.md#list-payments). A `store_order_id` matches at most one order in each mode. An invalid `limit` is `400 invalid_limit`; a cursor that isn't one of your orders in this mode is `400 invalid_cursor`.

## Update an order

`PATCH /v1/orders/:id`

Records the order's fulfilment. This is how your store tells Railbed an order has shipped, or that it's waiting again. Marking it `fulfilled` or `unfulfilled` appears on the order's timeline in the dashboard.

| Field | Type | Description |
|---|---|---|
| `fulfillment` | string · required | `none`, `unfulfilled` or `fulfilled` |

Example: cURL

```bash
curl -X PATCH \
  https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa \
  -H "Authorization: Bearer $RAILBED_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "fulfillment": "fulfilled" }'
```

Example: Node\.js

```js
const order = await fetch(`https://pay.railbed.io/v1/orders/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ fulfillment: 'fulfilled' }),
}).then((r) => r.json());
```

Example: Python

```python
order = requests.patch(
    f"https://pay.railbed.io/v1/orders/{id}",
    headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
    json={"fulfillment": "fulfilled"},
    timeout=15,
).json()
```

Example: PHP

```php
<?php
$ch = curl_init('https://pay.railbed.io/v1/orders/' . rawurlencode($id));
curl_setopt_array($ch, [
  CURLOPT_CUSTOMREQUEST => 'PATCH',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('RAILBED_SECRET_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode(['fulfillment' => 'fulfilled']),
]);
$order = json_decode(curl_exec($ch), true);
```

The response is the updated [order](#the-order-object).

- `fulfilled` sets `fulfilled_at` to now; `unfulfilled` and `none` clear it. Sending the value the order already has changes nothing, so retrying is safe.
- It works on any order of yours in this mode, from any source, paid or not. Railbed doesn't check the payment here, so report only what you've actually shipped.
- No webhook is sent for a fulfilment change.

| Status | Code | When |
|---|---|---|
| 400 | `invalid_fulfillment` | `fulfillment` is missing or not one of the three |
| 404 | `not_found` | Not an order of yours in this mode |
