# Orders

An order is created by checkout (or by accepting a quote) in status `pending_payment`. From the storefront API orders are read-only: all status changes (payment, shipment, refunds) happen merchant-side in the dashboard. Both endpoints require the `commerce:read` scope and the commerce module.

## Order status

| Status | Meaning | Can transition to |
| --- | --- | --- |
| `pending_payment` | Created, awaiting payment. | `paid`, `cancelled` |
| `paid` | Payment received. | `shipped`, `cancelled`, `refunded` |
| `shipped` | Handed to the carrier; tracking fields may be set. | `completed`, `refunded` |
| `completed` | Fulfilled. | `refunded` |
| `cancelled` | Cancelled before fulfillment. Inventory is restocked. | terminal |
| `refunded` | Refunded. Inventory is restocked. | terminal |

Treat the enum as open: a client should render an unknown future status neutrally rather than fail.

## The order response shape

Both endpoints serialize orders identically (single orders wrapped in `{"data": {...}}`, history entries as elements of a paginated `data` array). All money values are objects `{"amount": int, "currency": "EUR"}` with `amount` in minor units. Dates are ISO-8601 strings.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (UUID) | always | Order id. |
| `number` | integer | always | Human-facing order number from a per-store counter. Plain integer, not a UUID. |
| `status` | string | always | See the status table above. |
| `email` | string | always | The email given at checkout. |
| `billing_address` | object | always | The billing address as submitted (`name`, `street`, `postal_code`, `city`, `country`). |
| `shipping_address` | object or null | always | The shipping address, or null when shipping to the billing address. |
| `currency` | string | always | `EUR`. |
| `lines` | array | always | Order lines, see below. |
| `subtotal` | money | always | Goods total excluding tax. |
| `tax_total` | money | always | Total tax including shipping tax. |
| `total` | money | always | Grand total including shipping. |
| `tax_breakdown` | array | always | Per-rate breakdown, each `{"rate": float, "base": int, "tax": int}`. |
| `shipping_method_name` | string or null | always | Name snapshot of the chosen shipping method, null when no shipping applied. |
| `shipping` | money | always | Shipping cost excluding tax. `amount` is null when no shipping applied. |
| `shipping_tax` | money | always | Tax on shipping. `amount` is null when no shipping applied. |
| `tracking_carrier` | string or null | always | Set by the merchant when shipping. |
| `tracking_number` | string or null | always | Set by the merchant when shipping. |
| `invoice_number` | integer or null | always | Per-store invoice counter, set when the merchant invoices the order. |
| `invoiced_at` | string (ISO-8601) or null | always | When the order was invoiced. |
| `customer_id` | string (UUID) or null | always | The customer the order is bound to, null for guest orders. |
| `placed_at` | string (ISO-8601) or null | always | When the order was placed. |
| `created_at` | string (ISO-8601) or null | always | Row creation time. |

### `lines[]`

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (UUID) | always | Order line id. |
| `product_id` | string (UUID) | always | The product. |
| `variant_id` | string (UUID) or null | always | The variant, when applicable. |
| `variant_label` | string or null | always | Human-readable variant label snapshot, for example `"Green / M"`. |
| `name` | string | always | Product name snapshot. |
| `sku` | string or null | always | SKU snapshot. |
| `quantity` | integer | always | Quantity ordered. |
| `unit_price` | money | always | Unit price. |
| `line_subtotal` | money | always | Line total excluding tax. |
| `tax_rate` | number | always | Tax rate percentage applied to the line. |
| `tax` | money | always | Tax on the line. |
| `line_total` | money | always | Line total including tax. |
| `attributes` | object or null | always | Null for plain lines; the configuration snapshot for configurable products. |

## GET /v1/api/commerce/orders/{id}

Fetches a single order with lines by id. There is no customer-ownership check on this endpoint: it is authorized purely by the tenant API key, which is a server-side secret. It exists for the storefront server, for example rendering the order-confirmation page right after checkout. Never proxy it to browsers with a client-supplied order id unless your server performs its own ownership check first.

### Parameters

**Path**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (UUID) | yes | Order id. |

```bash title="Request"
curl https://api.borkol.com/v1/api/commerce/orders/7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1 \
  -H "Authorization: Bearer {api_key}"
```

```json title="Response"
{
  "data": {
    "id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
    "number": 1042,
    "status": "paid",
    "email": "jane@example.com",
    "billing_address": {
      "name": "Jane Doe",
      "street": "Keizersgracht 1",
      "postal_code": "1015 CC",
      "city": "Amsterdam",
      "country": "NL"
    },
    "shipping_address": null,
    "currency": "EUR",
    "lines": [
      {
        "id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "variant_label": "Green / M",
        "name": "Garden Chair",
        "sku": "CHAIR-GRN-M",
        "quantity": 2,
        "unit_price": { "amount": 2250, "currency": "EUR" },
        "line_subtotal": { "amount": 3347, "currency": "EUR" },
        "tax_rate": 21,
        "tax": { "amount": 703, "currency": "EUR" },
        "line_total": { "amount": 4050, "currency": "EUR" },
        "attributes": null
      }
    ],
    "subtotal": { "amount": 3347, "currency": "EUR" },
    "tax_total": { "amount": 789, "currency": "EUR" },
    "total": { "amount": 4545, "currency": "EUR" },
    "tax_breakdown": [{ "rate": 21, "base": 3756, "tax": 789 }],
    "shipping_method_name": "Standard",
    "shipping": { "amount": 409, "currency": "EUR" },
    "shipping_tax": { "amount": 86, "currency": "EUR" },
    "tracking_carrier": "PostNL",
    "tracking_number": "3STEST123456",
    "invoice_number": null,
    "invoiced_at": null,
    "customer_id": null,
    "placed_at": "2026-08-14T10:15:30+00:00",
    "created_at": "2026-08-14T10:15:30+00:00"
  }
}
```

### Errors

| Status | Body | When |
| --- | --- | --- |
| 404 | `{"message": "..."}` | Unknown or soft-deleted order id, or an order of another store. |
| 403 | scope, module, or suspension body | Key lacks `commerce:read`, module disabled, or store suspended. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit exceeded. |

## GET /v1/api/commerce/my/orders

Paginated order history for the customer identified by `X-Customer-Token`, sorted by `placed_at` descending (newest first). The token is mandatory: this is the endpoint behind a storefront's "my orders" page. Uses the standard Laravel pagination envelope.

### Parameters

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `per_page` | integer | no | Page size, default 25, clamped to 1..100. |
| `page` | integer | no | Page number, default 1. |

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | yes | Customer session token. Missing or unresolvable: 401. |

```bash title="Request"
curl "https://api.borkol.com/v1/api/commerce/my/orders?per_page=25&page=1" \
  -H "Authorization: Bearer {api_key}" \
  -H "X-Customer-Token: {customer_token}"
```

```javascript title="Request (JS)"
const res = await fetch(
  "https://api.borkol.com/v1/api/commerce/my/orders?per_page=25",
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "X-Customer-Token": customerToken,
    },
  }
);
const { data: orders, meta } = await res.json();
```

```json title="Response"
{
  "data": [
    {
      "id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
      "number": 1042,
      "status": "shipped",
      "email": "jane@example.com",
      "billing_address": {
        "name": "Jane Doe",
        "street": "Keizersgracht 1",
        "postal_code": "1015 CC",
        "city": "Amsterdam",
        "country": "NL"
      },
      "shipping_address": null,
      "currency": "EUR",
      "lines": [
        {
          "id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
          "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "variant_id": null,
          "variant_label": null,
          "name": "Garden Chair",
          "sku": "CHAIR-GRN",
          "quantity": 2,
          "unit_price": { "amount": 2250, "currency": "EUR" },
          "line_subtotal": { "amount": 3719, "currency": "EUR" },
          "tax_rate": 21,
          "tax": { "amount": 781, "currency": "EUR" },
          "line_total": { "amount": 4500, "currency": "EUR" },
          "attributes": null
        }
      ],
      "subtotal": { "amount": 3719, "currency": "EUR" },
      "tax_total": { "amount": 867, "currency": "EUR" },
      "total": { "amount": 4995, "currency": "EUR" },
      "tax_breakdown": [{ "rate": 21, "base": 4128, "tax": 867 }],
      "shipping_method_name": "Standard",
      "shipping": { "amount": 409, "currency": "EUR" },
      "shipping_tax": { "amount": 86, "currency": "EUR" },
      "tracking_carrier": "PostNL",
      "tracking_number": "3STEST123456",
      "invoice_number": null,
      "invoiced_at": null,
      "customer_id": "0a1b2c3d-4e5f-4a6b-7c8d-9e0f1a2b3c4d",
      "placed_at": "2026-08-10T14:02:11+00:00",
      "created_at": "2026-08-10T14:02:11+00:00"
    }
  ],
  "links": {
    "first": "https://api.borkol.com/v1/api/commerce/my/orders?page=1",
    "last": "https://api.borkol.com/v1/api/commerce/my/orders?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      { "url": null, "label": "&laquo; Previous", "active": false },
      { "url": "https://api.borkol.com/v1/api/commerce/my/orders?page=1", "label": "1", "active": true },
      { "url": null, "label": "Next &raquo;", "active": false }
    ],
    "path": "https://api.borkol.com/v1/api/commerce/my/orders",
    "per_page": 25,
    "to": 1,
    "total": 1
  }
}
```

### Errors

| Status | Body | When |
| --- | --- | --- |
| 401 | `{"message": "Invalid customer token."}` | `X-Customer-Token` missing or unresolvable. |
| 403 | scope, module, or suspension body | Key lacks `commerce:read`, module disabled, or store suspended. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit exceeded. |
