---
title: Orders
description: How Spree models orders — a permanent record created by completing a cart, with separate payment and delivery statuses.
---

## Overview

An order is a placed purchase. It's created by completing a [Cart](carts.md) and holds its own copy of everything the customer agreed to — items, prices, taxes, discounts and delivery.

```mermaid
erDiagram
    Order ||--o{ LineItem : "has many"
    Order ||--o{ Fulfillment : "has many"
    Order ||--o{ Payment : "has many"
    Order ||--o{ Return : "has many"
    Order }o--|| Cart : "created from"
    Order }o--|| Customer : "belongs to"

    Order {
        string number
        string payment_status
        string fulfillment_status
        string total
        datetime completed_at
    }
```

## Order attributes

| Attribute | Description |
|---|---|
| `id` | Order ID, e.g. `or_86Rf07xd4z` |
| `number` | Order number shown to customers, e.g. `R1001` — sequential by default, [customizable](../how-to/custom-document-numbers.md) |
| `cart_id` | The cart this order came from |
| `email` | Customer's email address |
| `currency` | Order currency, e.g. `USD` |
| `total_quantity` | Total number of items |
| `payment_status` | See [statuses](#statuses) below |
| `fulfillment_status` | See [statuses](#statuses) below |
| `item_total` / `display_item_total` | Sum of line item prices |
| `delivery_total` / `display_delivery_total` | Delivery cost |
| `tax_total` / `display_tax_total` | Total tax |
| `discount_total` / `display_discount_total` | Total discount |
| `total` / `display_total` | Order total |
| `amount_due` / `display_amount_due` | Still outstanding |
| `completed_at` | When the order was placed |

Every amount comes in two forms: `total` is the raw value (`"135.60"`) and [`display_total` is formatted for the currency](../../api-reference/store-api/monetary-amounts.md) (`"$135.60"`). Render the `display_` one.

## Statuses

An order tracks payment and delivery separately, because they genuinely move at different speeds. An order can be paid but not yet sent, or sent and later partly refunded.

**`payment_status`**

| Value | Meaning |
|---|---|
| `none` | Nothing paid or authorized yet |
| `authorized` | Money is held but not taken |
| `partially_paid` | Some of the total has been taken |
| `paid` | Fully paid |
| `partially_refunded` | Some money returned |
| `refunded` | Everything returned |
| `overcharged` | More was taken than the order total |
| `voided` | The hold was released without taking money |

**`fulfillment_status`**

| Value | Meaning |
|---|---|
| `unfulfilled` | Nothing has gone out yet |
| `backorder` | Waiting on stock |
| `partial` | Some of it has gone out |
| `fulfilled` | Everything has gone out |
| `delivered` | The customer has all of it |
| `canceled` | Nothing will be sent |

A canceled parcel is ignored while others are still live, so an order whose
second parcel was recalled is described by the first.

`delivered` means every parcel arrived. A fulfillment that travels as several
consignments — a machine in three boxes, a pallet under one freight number —
is only delivered when the last of them lands, and the order follows from
there. See [where the parcel actually
is](fulfillments.md#where-the-parcel-actually-is).

Both are worked out automatically from the order's payments, refunds and fulfillments. You change an order's status by acting on those — taking a payment, sending a parcel, issuing a refund — never by setting the status yourself.

## Reading an order

A customer can fetch their own order; guests use the order token they got at checkout.


```typescript Store SDK
// A single order
const order = await client.orders.get('or_xxx')

// The signed-in customer's order history
const orders = await client.customers.orders.list({ per_page: 20 })
```

```bash cURL
curl 'https://api.mystore.com/api/v3/store/orders/or_xxx' \
  -H 'X-Spree-API-Key: pk_xxx' \
  -H 'X-Spree-Token: order_token_xxx'
```


## Managing orders

Back-office work happens through the Admin API.


```typescript Admin SDK
// Find orders needing attention
const orders = await adminClient.orders.list({
  payment_status_eq: 'paid',
  fulfillment_status_eq: 'unfulfilled',
})

// Cancel an order, recording why from your own list of reasons
const [reason] = (await adminClient.orderCancellationReasons.list()).data
await adminClient.orders.cancel('or_xxx', {
  cancel_reason_id: reason.id,
  cancel_note: 'Supplier could not deliver in time',
})

// On a split checkout the payment is shared, so refund_payments decides
// whether this order's share of it comes back
await adminClient.orders.cancel('or_grouped_xxx', {
  cancel_reason_id: reason.id,
  refund_payments: true,
})

// On a split checkout the payment is shared, so this gives back only this
// order's share — all of it, or the part named by refund_amount
await adminClient.orders.cancel('or_grouped_xxx', {
  cancel_reason_id: reason.id,
  refund_payments: true,
  refund_amount: '25.00',
})

// Take an authorized payment
await adminClient.orders.payments.capture('or_xxx', 'pay_xxx')

// Refund against a specific payment
await adminClient.orders.refunds.create('or_xxx', {
  payment_id: 'pay_xxx',
  amount: '25.00',
})
```

```bash cURL
curl 'https://api.mystore.com/api/v3/admin/orders?filter[payment_status_eq]=paid' \
  -H 'X-Spree-API-Key: sk_xxx'

curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/cancel' \
  -H 'X-Spree-API-Key: sk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{ "cancel_reason_id": "ocr_xxx", "refund_payments": true }'
```


Canceling always puts stock back — the goods are not going out either way — and
every payment is settled at the gateway. What that means is the gateway's
decision: an authorization it never drew on is released, and a captured charge
it cannot release is refunded instead.

On a split checkout one payment is shared across several orders, so what comes
back is this order's own share, drawn from each payment in turn until it is met.
`refund_amount` names part of that share to return, keeping a restocking fee
back, say. It is refused on an ordinary order rather than ignored: the gateway
returns the whole captured payment there, so a cap could not be honoured, and
accepting one would refund everything while you believed part was held back.

It records who canceled, and optionally why: the reason comes from a list you
manage yourself under Settings, so it can say what your team actually means. A
staff-facing note can go alongside it.

## Orders don't change quietly

A placed order is meant to stay put. Its items and prices are written when the cart is completed and then left alone, so an order keeps saying what the customer actually agreed to — even if a product's price changes the next day.

Where admin edits are allowed, totals are re-added from the order's existing rows rather than worked out again from scratch. Editing an order must never quietly re-apply today's promotions and hand the customer a different discount than the one they accepted.

## Money on an order

Tax, discounts and fees are kept as separate records, so you can ask what tax was charged without picking through a mixed list. See [Order totals](order-totals.md).

## Events

Orders publish [events](events.md) — `order.placed`, `order.canceled`, `order.paid` and more — which also reach [webhooks](webhooks.md). This is the right way to push orders to another system, trigger fulfillment, or start an email flow.

## Related

- [Carts](carts.md) — shopping and checkout
- [Fulfillments](fulfillments.md) — getting items to the customer
- [Returns, Exchanges & Claims](returns-exchanges-claims.md) — after the sale
- [Payments](payments.md) — payment methods and processing
