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.
On this page
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 |
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 |
fulfilled_at | integer or null | When it was marked fulfilled, in Unix seconds |
customer_id | string or null | Its customer, cus_…. Null until the buyer has given an email |
currency | string | The currency of every amount on the order |
subtotal | string | The items' amounts 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 |
shipping_address | object or null | Where to send it. See 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 |
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.
{
"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
orderandcustomerwhen you create a session, and the order has your items, totals and addresses. Without them, the order has one line, the session'sdescription, for the whole amount. - From your store's order id. A session sent with
order.idjoins 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. - 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:
paidwhen its paid payments covertotal(they add up to at leasttotalin the order'scurrency).- Otherwise
heldwhen any of its payments is held for review, or is paid but doesn't covertotal(a part-payment after the order grew, or one in another currency). - Otherwise the latest payment's status:
open,pending,failedorexpired, orcanceledwhen 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.idget their fulfilment from your store, through Update an order. The dashboard shows it but doesn't change it, so the two never disagree. The WooCommerce plugin 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.
curl https://pay.railbed.io/v1/orders/ord_Vd3kR8mQ1xTn6LpZs0Wa \
-H "Authorization: Bearer $RAILBED_SECRET_KEY"const order = await fetch(`https://pay.railbed.io/v1/orders/${id}`, {
headers: { Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}` },
}).then((r) => r.json());order = requests.get(
f"https://pay.railbed.io/v1/orders/{id}",
headers={"Authorization": f"Bearer {os.environ['RAILBED_SECRET_KEY']}"},
timeout=15,
).json()<?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. To find the order behind a payment or webhook, use the payment's order_id (orderId in webhooks); webhooks also carry the order itself.
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 |
curl "https://pay.railbed.io/v1/orders?store_id=yourstore&store_order_id=1042" \
-H "Authorization: Bearer $RAILBED_SECRET_KEY"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;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<?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;{
"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 (trimmed here). Page through with next_cursor as on 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 |
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" }'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());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()<?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.
fulfilledsetsfulfilled_atto now;unfulfilledandnoneclear 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 |