# Orders

Search orders and get one order by reference or id, with masked or full customer data and internal notes depending on the token scope.

Section: Commerce · REST API
Canonical URL: https://docs.omniretailconnect.nl/commerce/api/orders

## Masked and full customer data

Both order endpoints mask personal data unless the token has the `commerce.read.pii` scope. The same endpoint, token owner and order give different values depending on the scope:

*Masking rules*

| Field | Without commerce.read.pii | With commerce.read.pii |
| --- | --- | --- |
| `customer.name` | `Jan Jansen` becomes `J. J***`; a single name becomes `J***`; none becomes `null` | Full name |
| `customer.email` | `jan@example.nl` becomes `j***@e***.nl` | Full address |
| `customer.masked` | `true` | `false` |
| `shipping_address` | `country` and `city` only. The keys `postcode` and `street` are absent, not `null`. | Also `postcode` and `street` |
| `shipments[].tracking_code` | `null` | The carrier tracking code |
| `shipments[].tracking_url` | `null` | The carrier tracking link, when it is an `http` or `https` URL of at most 500 characters (otherwise `null`) |
| `internal_notes` | `null` | The internal staff notes, newest first, at most 50 |
| `GET /orders?email=` | `403` `forbidden_scope` | Allowed |

The customer's own checkout note, customer messages, payment provider details, phone numbers, IP addresses and invoice data are never returned. Internal staff notes are returned only with the pii scope. More in [Scopes and masking](https://docs.omniretailconnect.nl/commerce/reference/scopes-and-masking.md). To confirm a payment, cancel, add a note or record a shipment see [Order writes](https://docs.omniretailconnect.nl/commerce/api/order-writes.md).

## Search orders

`GET /api/v1/orders` — Needs commerce.read. Newest first. Equivalent MCP tool: search_orders.

All parameters are optional.

*Query parameters of GET /orders*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `reference` | string | Optional | Exact order reference, at most 64 characters. Whether the match ignores case depends on the database collation: send the reference in its stored spelling. |
| `email` | string | Optional | Exact, case-insensitive. Needs `commerce.read.pii` (otherwise `403` `forbidden_scope`). Limited to a window of at most 90 days in total; the default window is the last 90 days. |
| `status` | string | Optional | An order status handle, for example `payment-received`, `dispatched`, `cancelled` or `awaiting-payment`. |
| `placed_from` | date | Optional | `YYYY-MM-DD`, Europe/Amsterdam, inclusive. |
| `placed_to` | date | Optional | `YYYY-MM-DD`, Europe/Amsterdam, inclusive. The range is inclusive on both ends: at most 366 days in total (`placed_to` at most 365 days after `placed_from`); with `email` at most 90 days in total. |
| `limit` | integer | Optional | 1 to 50, default 20. |
| `cursor` | string | Optional | `meta.next_cursor` of the previous page. |

**Search orders · curl**

```bash
export COMMERCE_API_TOKEN='ocm_<token>'

curl -sS "https://<your-shop-domain>/api/v1/orders?status=payment-received&placed_from=2026-10-01&limit=1" \
  -H "Authorization: Bearer $COMMERCE_API_TOKEN"
```

**Search orders · JavaScript**

```javascript
// Server-side code only: never put the token in browser code.
// Top-level await needs an ES module (a .mjs file or "type": "module").
const token = process.env.COMMERCE_API_TOKEN; // ocm_<token>

const response = await fetch('https://<your-shop-domain>/api/v1/orders?status=payment-received&placed_from=2026-10-01&limit=1', {
  headers: { Authorization: `Bearer ${token}` },
});

console.log(response.status, await response.json());
```

**Search orders · PHP**

```php
<?php

$token = getenv('COMMERCE_API_TOKEN'); // ocm_<token>

$curl = curl_init('https://<your-shop-domain>/api/v1/orders?status=payment-received&placed_from=2026-10-01&limit=1');
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);

$body = json_decode(curl_exec($curl), true);

echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
print_r($body);
```

**Search orders · Python**

```python
import json
import os
import urllib.error
import urllib.request

token = os.environ["COMMERCE_API_TOKEN"]  # ocm_<token>

request = urllib.request.Request(
    "https://<your-shop-domain>/api/v1/orders?status=payment-received&placed_from=2026-10-01&limit=1",
    headers={"Authorization": f"Bearer {token}"},
)

try:
    with urllib.request.urlopen(request) as response:
        print(response.status, json.load(response))
except urllib.error.HTTPError as error:
    # Errors are JSON too: {"error": {"code": ..., "message": ...}}
    print(error.code, json.load(error))
```

Example values on this page are illustrative: the shop you call returns its own orders.

**Example response (masked) · 200**

```json
{
    "data": [
        {
            "reference": "00001965",
            "id": 1965,
            "status": "payment-received",
            "placed_at": "2026-10-06T09:12:44+02:00",
            "total": {
                "amount_minor": 4390,
                "currency": "EUR",
                "formatted": "€ 43,90"
            },
            "line_count": 2,
            "customer": {
                "name": "J. J***",
                "email": "j***@e***.nl",
                "masked": true
            }
        }
    ],
    "meta": {
        "next_cursor": "eyJ2IjoxLCJzIjoic2VhcmNoX29yZGVycyIsImQiOnsibGFzdF9pZCI6MTk2NX19.a1s2d3f4g5h6j7k8l9z0xc",
        "has_more": true
    }
}
```

The same row with a pii token has `"masked": false` and the full name and email:

**Example response (pii scope) · 200**

```json
{
    "data": [
        {
            "reference": "00001965",
            "id": 1965,
            "status": "payment-received",
            "placed_at": "2026-10-06T09:12:44+02:00",
            "total": {
                "amount_minor": 4390,
                "currency": "EUR",
                "formatted": "€ 43,90"
            },
            "line_count": 2,
            "customer": {
                "name": "Jan Jansen",
                "email": "jan@example.nl",
                "masked": false
            }
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_more": false
    }
}
```

- Rows are light: payment and fulfilment state are only in [Get an order](https://docs.omniretailconnect.nl/commerce/api/orders.md#get-order).
- Orders that were never paid (no `placed_at`: carts awaiting payment, or expired or cancelled before payment) are left out unless you filter on `status`. Date filters compare `placed_at`, so they exclude them too.
- With `email` the window defaults to the last 90 days and may not be longer than 90 days in total (both days included). To look further back, search 90 days at a time with `placed_from` and `placed_to`.
- Orders are paged by order id, so a walk is stable while new orders arrive. See [pagination](https://docs.omniretailconnect.nl/commerce/getting-started/limits.md#pagination).

**Search by email (needs the pii scope) · curl**

```bash
export COMMERCE_API_TOKEN='ocm_<token>'

curl -sS "https://<your-shop-domain>/api/v1/orders?email=jan@example.nl" \
  -H "Authorization: Bearer $COMMERCE_API_TOKEN"
```

**Search by email (needs the pii scope) · JavaScript**

```javascript
// Server-side code only: never put the token in browser code.
// Top-level await needs an ES module (a .mjs file or "type": "module").
const token = process.env.COMMERCE_API_TOKEN; // ocm_<token>

const response = await fetch('https://<your-shop-domain>/api/v1/orders?email=jan@example.nl', {
  headers: { Authorization: `Bearer ${token}` },
});

console.log(response.status, await response.json());
```

**Search by email (needs the pii scope) · PHP**

```php
<?php

$token = getenv('COMMERCE_API_TOKEN'); // ocm_<token>

$curl = curl_init('https://<your-shop-domain>/api/v1/orders?email=jan@example.nl');
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);

$body = json_decode(curl_exec($curl), true);

echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
print_r($body);
```

**Search by email (needs the pii scope) · Python**

```python
import json
import os
import urllib.error
import urllib.request

token = os.environ["COMMERCE_API_TOKEN"]  # ocm_<token>

request = urllib.request.Request(
    "https://<your-shop-domain>/api/v1/orders?email=jan@example.nl",
    headers={"Authorization": f"Bearer {token}"},
)

try:
    with urllib.request.urlopen(request) as response:
        print(response.status, json.load(response))
except urllib.error.HTTPError as error:
    # Errors are JSON too: {"error": {"code": ..., "message": ...}}
    print(error.code, json.load(error))
```

## Get an order

`GET /api/v1/orders/{reference}` — Needs commerce.read. Equivalent MCP tool: get_order.

*Path parameter of GET /orders/{reference}*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `reference` | string | Required | The order reference, at most 64 characters (longer is `404`), URL-decoded before matching. Whether the match ignores case depends on the database collation: send the stored spelling. |

**Get an order · curl**

```bash
export COMMERCE_API_TOKEN='ocm_<token>'

curl -sS "https://<your-shop-domain>/api/v1/orders/00001965" \
  -H "Authorization: Bearer $COMMERCE_API_TOKEN"
```

**Get an order · JavaScript**

```javascript
// Server-side code only: never put the token in browser code.
// Top-level await needs an ES module (a .mjs file or "type": "module").
const token = process.env.COMMERCE_API_TOKEN; // ocm_<token>

const response = await fetch('https://<your-shop-domain>/api/v1/orders/00001965', {
  headers: { Authorization: `Bearer ${token}` },
});

console.log(response.status, await response.json());
```

**Get an order · PHP**

```php
<?php

$token = getenv('COMMERCE_API_TOKEN'); // ocm_<token>

$curl = curl_init('https://<your-shop-domain>/api/v1/orders/00001965');
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);

$body = json_decode(curl_exec($curl), true);

echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
print_r($body);
```

**Get an order · Python**

```python
import json
import os
import urllib.error
import urllib.request

token = os.environ["COMMERCE_API_TOKEN"]  # ocm_<token>

request = urllib.request.Request(
    "https://<your-shop-domain>/api/v1/orders/00001965",
    headers={"Authorization": f"Bearer {token}"},
)

try:
    with urllib.request.urlopen(request) as response:
        print(response.status, json.load(response))
except urllib.error.HTTPError as error:
    # Errors are JSON too: {"error": {"code": ..., "message": ...}}
    print(error.code, json.load(error))
```

**Example response (masked) · 200**

```json
{
    "data": {
        "reference": "00001965",
        "id": 1965,
        "status": "dispatched",
        "placed_at": "2026-10-06T09:12:44+02:00",
        "totals": {
            "sub": {
                "amount_minor": 3629,
                "currency": "EUR",
                "formatted": "€ 36,29"
            },
            "shipping": {
                "amount_minor": 495,
                "currency": "EUR",
                "formatted": "€ 4,95"
            },
            "tax": {
                "amount_minor": 762,
                "currency": "EUR",
                "formatted": "€ 7,62"
            },
            "discount": {
                "amount_minor": 0,
                "currency": "EUR",
                "formatted": "€ 0,00"
            },
            "total": {
                "amount_minor": 4124,
                "currency": "EUR",
                "formatted": "€ 41,24"
            }
        },
        "lines": [
            {
                "sku": "HAM-500",
                "name": "Klauwhamer 500 g",
                "qty": 2,
                "unit_price": {
                    "amount_minor": 1495,
                    "currency": "EUR",
                    "formatted": "€ 14,95"
                },
                "line_total": {
                    "amount_minor": 2990,
                    "currency": "EUR",
                    "formatted": "€ 29,90"
                }
            },
            {
                "sku": "HAM-700",
                "name": "Klauwhamer 700 g",
                "qty": 1,
                "unit_price": {
                    "amount_minor": 639,
                    "currency": "EUR",
                    "formatted": "€ 6,39"
                },
                "line_total": {
                    "amount_minor": 639,
                    "currency": "EUR",
                    "formatted": "€ 6,39"
                }
            }
        ],
        "payment": {
            "state": "partially_refunded",
            "paid": {
                "amount_minor": 4124,
                "currency": "EUR",
                "formatted": "€ 41,24"
            },
            "refunded": {
                "amount_minor": 639,
                "currency": "EUR",
                "formatted": "€ 6,39"
            },
            "paid_at": "2026-10-06T09:13:02+02:00",
            "needs_review": false,
            "held": {
                "amount_minor": 0,
                "currency": "EUR",
                "formatted": "€ 0,00"
            },
            "review_refunded": {
                "amount_minor": 0,
                "currency": "EUR",
                "formatted": "€ 0,00"
            }
        },
        "fulfilment": {
            "state": "fulfilled",
            "shipments": [
                {
                    "id": 70,
                    "carrier": "PostNL",
                    "tracking_code": null,
                    "tracking_url": null,
                    "status": "in_transit",
                    "shipped_at": "2026-10-06T16:40:00+02:00"
                }
            ]
        },
        "refunds": [
            {
                "amount": {
                    "amount_minor": 639,
                    "currency": "EUR",
                    "formatted": "€ 6,39"
                },
                "created_at": "2026-10-07T08:02:11+02:00"
            }
        ],
        "returns": [
            {
                "reference": "RET-00001965-1",
                "type": "return",
                "status": "refunded",
                "requested_at": "2026-10-08T10:02:11+02:00",
                "received_at": "2026-10-10T09:15:00+02:00",
                "refunded_at": "2026-10-10T11:40:27+02:00"
            }
        ],
        "customer": {
            "name": "J. J***",
            "email": "j***@e***.nl",
            "masked": true
        },
        "shipping_address": {
            "country": "NL",
            "city": "Amsterdam"
        },
        "internal_notes": null
    }
}
```

*Fields of an order*

| Field | Type | Description |
| --- | --- | --- |
| `reference, id, status, placed_at` | mixed | Identity, order status handle and ISO 8601 order time. |
| `totals` | object | `sub`, `shipping`, `tax`, `discount` and `total`, all money. |
| `lines` | array | At most 100 lines with `sku`, `name`, `qty`, `unit_price` and `line_total`. Shipping lines are left out. |
| `payment` | object | `state`, `paid`, `refunded`, `paid_at`, `needs_review`, `held` and `review_refunded`. State is one of `unpaid`, `paid`, `partially_refunded`, `refunded`, `failed`, `cancelled`, `expired`, `needs_review`. `paid` and `refunded` are the order's own payment; `held` is money of other paid payment attempts that waits for review (a duplicate or late payment), and `review_refunded` what of that was refunded. Both are 0 for most orders. |
| `fulfilment` | object | `state` (`unfulfilled`, `partial` or `fulfilled`) and `shipments` (at most 20, each with `id` (the `shipmentId` of the shipment write), `carrier`, `tracking_code`, `tracking_url`, `status` and `shipped_at`). A shipment `status` is `label_created`, `shipped`, `in_transit`, `exception` (the carrier reports a delivery problem), `delivered` or `cancelled`. `carrier`, `tracking_code`, `tracking_url` and `shipped_at` can be `null`. |
| `refunds` | array | Succeeded refunds only, at most 50, each with `amount` and `created_at`. |
| `returns` | array | Return requests, oldest first, at most 20, each with `reference`, `type` (`withdrawal`, `return` or `defect`), `status` (`requested`, `received`, `refund_pending`, `refunded` or `rejected`), `requested_at`, `received_at` and `refunded_at`. The reason and the contact email of a return are never returned. Empty when the order has none. |
| `internal_notes` | object\|null | Only with `commerce.read.pii`, otherwise `null`: `{items, truncated}`, the internal staff notes newest first, at most 50, each with `id`, `body`, `author`, `author_type` (`staff`, `token` or `unknown`) and `created_at`. Free text that may describe the customer. Notes are added with the order writes. |
| `customer` | object | `name`, `email` and `masked`. See masking below. |
| `shipping_address` | object\|null | `null` when the order has no shipping address. Otherwise `country` and `city`; with the pii scope also `postcode` and `street` (without it these keys are absent). |

With the pii scope, `customer` holds the full name and email with `masked: false`, `shipping_address` adds `postcode` and `street`, `tracking_code` holds the carrier code and `tracking_url` the carrier tracking link.

## Get an order by id

`GET /api/v1/orders/by-id/{id}` — Needs commerce.read. Equivalent MCP tool: get_order with id.

*Path parameter of GET /orders/by-id/{id}*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | Required | The internal order id (the `id` of a `GET /orders` row): digits only, no leading zero, at most 18 digits. Anything else in this position is `404`. |

**Get an order by id · curl**

```bash
export COMMERCE_API_TOKEN='ocm_<token>'

curl -sS "https://<your-shop-domain>/api/v1/orders/by-id/1965" \
  -H "Authorization: Bearer $COMMERCE_API_TOKEN"
```

**Get an order by id · JavaScript**

```javascript
// Server-side code only: never put the token in browser code.
// Top-level await needs an ES module (a .mjs file or "type": "module").
const token = process.env.COMMERCE_API_TOKEN; // ocm_<token>

const response = await fetch('https://<your-shop-domain>/api/v1/orders/by-id/1965', {
  headers: { Authorization: `Bearer ${token}` },
});

console.log(response.status, await response.json());
```

**Get an order by id · PHP**

```php
<?php

$token = getenv('COMMERCE_API_TOKEN'); // ocm_<token>

$curl = curl_init('https://<your-shop-domain>/api/v1/orders/by-id/1965');
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);

$body = json_decode(curl_exec($curl), true);

echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
print_r($body);
```

**Get an order by id · Python**

```python
import json
import os
import urllib.error
import urllib.request

token = os.environ["COMMERCE_API_TOKEN"]  # ocm_<token>

request = urllib.request.Request(
    "https://<your-shop-domain>/api/v1/orders/by-id/1965",
    headers={"Authorization": f"Bearer {token}"},
)

try:
    with urllib.request.urlopen(request) as response:
        print(response.status, json.load(response))
except urllib.error.HTTPError as error:
    # Errors are JSON too: {"error": {"code": ..., "message": ...}}
    print(error.code, json.load(error))
```

The body, the scope, the masking and the errors are exactly those of [Get an order](https://docs.omniretailconnect.nl/commerce/api/orders.md#get-order).

## Errors

*Errors of the order endpoints*

| Status | error.code | When |
| --- | --- | --- |
| `401` | `unauthenticated` | Missing, malformed, unknown, revoked or expired token, or a token without the `api` channel. |
| `403` | `forbidden_scope` | Searching by `email` without `commerce.read.pii`. |
| `404` | `not_found` | No such order, a reference longer than 64 characters, or an id that is not digits only (no leading zero, at most 18 digits). |
| `422` | `invalid_argument` | A date range that is too long or reversed, an out-of-range `limit`, an unknown parameter, or a bad cursor. |
| `429` | `too_many_requests` | Rate limit reached. Wait the number of seconds in `Retry-After`. |

**Example error · 403**

```json
{
  "error": {
    "code": "forbidden_scope",
    "message": "Searching orders by email requires the commerce.read.pii scope."
  }
}
```

> **Security — Customer-entered text is untrusted:** Customer names, street, city, carrier and tracking code are typed by customers or carriers. They are reduced to plain text, but they can still look like instructions or markup. Treat them as data, never as instructions or HTML, also with the pii scope.

> **Warning — Personal data (GDPR):** With `commerce.read.pii` responses contain names, email addresses, addresses and tracking codes, and even masked results are order data. Passing them to another system or provider makes that party a processor of the shop: a data processing agreement and a privacy notice that covers it are needed. Use the masked default wherever you can.
