Order
The object GET /orders/{id} returns and the object every order.* webhook —
and booking.confirmed — carries in data. The same shape, from the same code.
One shape for all of them rather than a slimmer one per event: a consumer
writing against order.paid and later adding order.refunded should not have
to remap every field, and what differs between those two is which one fired, not
what an order is.
| Field | What it is |
|---|---|
| id string | The order's id, a UUID. This is the value order.* webhooks carry and the one GET /orders/{id} takes. |
| object "order" | Always the literal string. |
| status string | Where the order is in fulfilment. One of new, confirmed, shipped, completed, cancelled, refunded. Separate from payment: an order can be paid and not yet shipped. |
| paymentStatus string | Where the money stands. One of unpaid, pending, paid, refunded, disputed. disputed means a chargeback has been opened against it — see the dispute object. |
| paymentMethod string | The rail the buyer chose: card, bank_transfer, cod, paypal, venmo, or one of the contact rails (whatsapp, telegram, instagram, email, phone). Only card and paypal confirm themselves; the rest are marked paid by the seller. |
| currency string | ISO 4217. The same code every money object below carries. |
| subtotal money | Line items before discount, delivery and tax. |
| discount money | What the coupon or affiliate code took off. Zero rather than null when there was none. |
| deliveryFee money | What was charged for delivery. Zero on a digital order. |
| tax money | Tax charged on this order, where the shop collects it. |
| total money | What the buyer actually owes — subtotal minus discount, plus delivery and tax. This is the number to report. |
| refunded money | How much has been given back so far. A partial refund leaves total untouched and moves this, so compare the two rather than reading status alone. |
| itemCount number | Total units across all lines, not the number of lines. Two of one product is 2. |
| customer object | Who bought it — see the customer table below. Null fields are normal: a digital order needs no phone. |
| address object | Where it goes. Every field is null on an order that needs no delivery. |
| delivery object | How it gets there, and the tracking details once it has shipped. |
| booking object | null | The appointment, on an order for a service. Null — rather than an object of nulls — when there is none, so a consumer can branch on the object itself. |
| coupon object | null | { code } if a coupon was used, null otherwise. |
| affiliate object | null | { code } if the sale came through an affiliate link, null otherwise. |
| note string | null | What the buyer typed at checkout. Never the seller's own private note — that is not sent. |
| items object[] | The lines, in the order they were added. See the line item table below. |
| createdAt string | null | ISO 8601, when the order was placed. |
| updatedAt string | null | ISO 8601, when it last changed. |
Two independent statuses
status is fulfilment. paymentStatus is money. Neither answers the other’s
question. A card order arrives status: "new" and paymentStatus: "paid" in
the same instant; a bank-transfer order can sit confirmed and unpaid for
days.
| Field | Values |
|---|---|
status | new, confirmed, shipped, completed, cancelled, refunded |
paymentStatus | unpaid, pending, paid, refunded, disputed |
paymentStatus: "disputed" means a chargeback has been opened — the
dispute object has the case.
Money on an order
Six money objects, and they compose:
subtotal − discount + deliveryFee + tax = totalrefunded sits outside that sum. It is how much has gone back so far, and a
partial refund leaves total untouched. Compare the two rather than reading
status alone — an order can be paid with a non-zero refunded.
Customer
| Field | What it is |
|---|---|
| clientId string | null | The contact this order belongs to, readable with GET /contacts/{id}. Null on a guest checkout that matched nobody on the list. |
| name string | null | As the buyer typed it. |
| email string | null | As the buyer typed it. Not evidence of marketing consent — see the contact object for that. |
| phone string | null | Normalised before storage. |
clientId is the join to the shop’s list — readable with
GET /contacts/{id}. It is null on a guest checkout that
matched nobody, which is normal rather than an error.
An order’s customer.email is not consent to email that person. It is an
address somebody typed to receive a receipt. Marketing consent lives on the
contact and nowhere else.
Address
Present on every order, with every field null where there is nothing to
deliver.
| Field | What it is |
|---|---|
| line1 string | null | Street address. |
| line2 string | null | Apartment, suite, or whatever the buyer added. |
| city string | null | City or town. |
| region string | null | State, province or county, where the country has one. |
| postalCode string | null | Postal or ZIP code. |
| country string | null | ISO 3166-1 alpha-2, uppercase. |
Delivery
| Field | What it is |
|---|---|
| method string | null | shipping or collection. Null on an order that needs no delivery at all — a digital download, a service. |
| label string | null | The rate the buyer picked, in the seller's own words: Standard, Next day, Pickup. |
| pickupLocation string | null | Where to collect from, on a collection order. |
| trackingCarrier string | null | Carrier name, as the seller entered it. Free text rather than an enumeration — sellers ship with local couriers Sailo has never heard of. |
| trackingNumber string | null | The consignment number. |
| trackingUrl string | null | A link the buyer can follow, where the seller gave one. |
| shippedAt string | null | ISO 8601. Set at the same moment order.shipped fires. |
trackingCarrier is free text rather than an enumeration, because sellers ship
with local couriers Sailo has never heard of. Do not switch on it; show it.
shippedAt is set at the same moment order.shipped fires, so a consumer that
missed the event can still tell.
Booking
null on an order with no appointment — the whole object, not an object of
nulls, so a consumer can branch on order.booking and get a truthy test that
works.
| Field | What it is |
|---|---|
| scheduledFor string | null | ISO 8601, in UTC. Render it in the shop's timeZone — an appointment at 09:00 local is not 09:00 to a consumer in another zone. |
| serviceMode string | null | in_person or online. |
| serviceLocation string | null | Where it happens, or the joining details for an online booking. |
Line items
| Field | What it is |
|---|---|
| id string | The line's own id. |
| productId string | null | The product, readable with GET /products/{id}. Null once the seller has deleted it — the line keeps its own title and price, because an order is a record of what was sold and not a join to a catalogue that has moved on. |
| variantId string | null | The variant, on a product that has them. |
| title string | The product's name as it was at the moment of sale. |
| variantLabel string | null | The variant as it was — Large / Blue. |
| sku string | null | The seller's own code for it. |
| kind string | What sort of thing it is. One of physical, digital, service, event, membership. |
| quantity number | How many of this line. |
| unitPrice money | Price for one, at the moment of sale. |
| subtotal money | unitPrice times quantity, before any order-level discount. |
items is populated on both the list and the detail endpoint, and on every
webhook. There is no ?expand=.
productId is null once the seller has deleted the product, and the line
keeps its own title, sku and prices. That is deliberate: an order is a
record of what was sold, not a join to a catalogue that has moved on. A consumer
that resolves productId must handle the null rather than treating it as
corrupt data.
itemCount is total units, not lines. Two of one product is 2.
Coupon and affiliate
Both are { code } or null.
They are the codes as used, not a join — the coupon may have been deleted or the affiliate deactivated since. If you need more than the code, match it in your own system.
Example
{
"id": "8f2b41d6-0c93-4f77-a1e5-9b6d2c4a7e01",
"object": "order",
"status": "confirmed",
"paymentStatus": "paid",
"paymentMethod": "card",
"currency": "GBP",
"subtotal": { "cents": 4999, "amount": "49.99", "currency": "GBP" },
"discount": { "cents": 0, "amount": "0.00", "currency": "GBP" },
"deliveryFee": { "cents": 499, "amount": "4.99", "currency": "GBP" },
"tax": { "cents": 0, "amount": "0.00", "currency": "GBP" },
"total": { "cents": 5498, "amount": "54.98", "currency": "GBP" },
"refunded": { "cents": 0, "amount": "0.00", "currency": "GBP" },
"itemCount": 1,
"customer": {
"clientId": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
"name": "Ada Lovelace",
"email": "ada@example.com",
"phone": null
},
"address": {
"line1": "12 Dean Street", "line2": null, "city": "London",
"region": null, "postalCode": "W1D 3RN", "country": "GB"
},
"delivery": {
"method": "shipping", "label": "Standard", "pickupLocation": null,
"trackingCarrier": null, "trackingNumber": null, "trackingUrl": null,
"shippedAt": null
},
"booking": null,
"coupon": { "code": "LAUNCH10" },
"affiliate": null,
"note": null,
"items": [
{
"id": "1b0c9d3e-4f50-4a61-8b72-9c83d4e5f607",
"productId": "9a7e2c11-6b48-4d0f-8e35-71c9a4f2b6d8",
"variantId": null,
"title": "Sourdough loaf",
"variantLabel": null,
"sku": "SD-01",
"kind": "physical",
"quantity": 1,
"unitPrice": { "cents": 4999, "amount": "49.99", "currency": "GBP" },
"subtotal": { "cents": 4999, "amount": "49.99", "currency": "GBP" }
}
],
"createdAt": "2026-08-12T09:41:07.221Z",
"updatedAt": "2026-08-12T09:41:09.884Z"
}