# Orders

Part of the [Salaf Commerce API reference](https://www.salafems.com/developers/reference.md). Paths below are shown exactly as the server routes them — prefix them with `https://api.salafems.com`. Authenticate every request with `Authorization: Bearer salaf_sk_YOUR_API_KEY`; each operation names the scope its key must hold. Every failure uses the one error envelope — see [Errors](https://www.salafems.com/developers/errors.md).

## List orders

`GET /ext/v1/orders` · Requires scope `orders:read`

Compact by default. Add `?expand=customer,items` for the full object. `updated_after` is the sync filter — it catches orders placed before your last poll but changed since.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | `number` (default `25`, max 100) | No | Page size. |
| `starting_after` | `string` | No | Return the page AFTER this object id (the previous page's `next_cursor`). |
| `ending_before` | `string` | No | Return the page BEFORE this object id. |
| `sort` | `string` (default `-created_at`) | No | Newest first by default. Only `created_at` is sortable — a cursor over a mutable key (like `updated_at`) cannot page reliably. Use `updated_after` to sync changes. One of: `created_at`, `-created_at`. |
| `store_id` | `string` | No | Restrict to one store. Required breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |
| `status` | `string` | No | Filter to one of the values below. One of: `pending`, `follow_up`, `confirmed`, `processing`, `ready_to_ship`, `in_transit`, `delivered`, `returned`, `failed_delivery`, `cancelled`, `hold`, `spam`. |
| `payment_status` | `string` | No | Filter to one of the values below. One of: `unpaid`, `partially_paid`, `paid`, `refunded`. |
| `fulfillment_status` | `string` | No | Filter to one of the values below. One of: `unfulfilled`, `partially_fulfilled`, `fulfilled`, `returned`. |
| `channel` | `string` | No | Sales channel SLUG (the stable per-store key), not its id. |
| `created_after` | `date-time` | No | ISO-8601 timestamp. Inclusive lower bound. |
| `created_before` | `date-time` | No | ISO-8601 timestamp. Exclusive upper bound. |
| `updated_after` | `date-time` | No | Only orders changed since this instant. THE sync filter: an order placed last month and confirmed this morning is exactly the row a `created_after` poll would miss. |
| `search` | `string` | No | Match on order number, customer name or customer phone. |
| `expand` | `string[]` | No | Include related objects. Allowed: `customer`, `items`. Comma-separated, one level. Omitted by default so a sync walking pages stays cheap. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/orders?limit=25&updated_after=2026-08-01T00%3A00%3A00Z&expand=customer%2Citems" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders");
Object.entries({
  limit: "25",
  updated_after: "2026-08-01T00:00:00Z",
  expand: "customer,items",
}).forEach(([key, value]) => url.searchParams.set(key, value));

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const { data, meta } = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.salafems.com/ext/v1/orders",
    params={
        "limit": "25",
        "updated_after": "2026-08-01T00:00:00Z",
        "expand": "customer,items",
    },
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

page = response.json()
rows, meta = page["data"], page["meta"]
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$ch = curl_init('https://api.salafems.com/ext/v1/orders?limit=25&updated_after=2026-08-01T00%3A00%3A00Z&expand=customer%2Citems');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}

$rows = $response['data'];
$meta = $response['meta'];
```

### Responses

- `200` — A cursor page of orders.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "data": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "number": "ORD-20260811-1042",
      "source": "api",
      "channel": {
        "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
        "name": "Facebook",
        "slug": "facebook",
        "type": "facebook"
      },
      "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "location_id": null,
      "status": "pending",
      "payment_status": "unpaid",
      "fulfillment_status": "unfulfilled",
      "subtotal": "1250.00",
      "discount_amount": "1250.00",
      "shipping_charge": "1250.00",
      "tax_amount": "1250.00",
      "total_amount": "1250.00",
      "paid_amount": "1250.00",
      "due_amount": "1250.00",
      "refunded_amount": "1250.00",
      "shipping_address": null,
      "coupon": null,
      "custom_fields": null,
      "note": null,
      "delivered_at": "2026-08-11T10:00:00.000Z",
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
  }
}
```

## Create an order

`POST /ext/v1/orders` · Requires scope `orders:write`

Runs the SAME order-creation core as the dashboard and storefront: availability-checked stock reservation, coupon engine, totals math and plan-quota metering are all inherited, never re-implemented. Lines reference variants by id or SKU; the customer is an existing id or a find-or-create by phone. Price overrides are honored — this is a trusted-merchant surface. Company-scoped keys must send `store_id`.

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | Yes | REQUIRED. A unique value per logical request (a UUID is ideal); reuse it verbatim when retrying. A retry replays the original response with `Idempotent-Replayed: true` instead of running twice. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `channel` | `string` | No | A sales-channel SLUG for attribution. Defaults to the store's `manual` channel (Decision Q11 — tagging is optional, never forced). |
| `location_id` | `uuid` | No | Location (warehouse) to allocate stock from. Defaults to the company's default location — the admin-desk rule. |
| `customer_id` | `uuid` | No | An existing customer. Provide this OR `customer`. |
| `customer` | `CreateOrderCustomerDto` | No | Find-or-create by phone. Provide this OR `customer_id`. |
| `items` | `CreateOrderLineDto[]` | Yes | — |
| `shipping_address` | `object` | No | Free-form shipping address object, stored verbatim. |
| `shipping_charge` | `number` | No | Delivery charge as YOUR system computed it (trusted surface). A free-shipping coupon still zeroes it. Default 0. |
| `tax_amount` | `number` | No | — |
| `coupon_code` | `string` | No | Priced and redeemed by the same engine as every other surface; an unusable code fails the order rather than mispricing it. |
| `note` | `string` | No | — |
| `payment` | `CreateOrderPaymentDto` | No | An upfront settled payment (like the admin desk's "paid amount"). Omit for an unpaid order; record later payments via `POST /v1/orders/{id}/payments`. |

### `customer` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | `string` | Yes | Bangladeshi mobile, any accepted spelling — stored canonical. THE find-or-create match key. |
| `full_name` | `string` | No | Required when the phone matches no existing customer (a new record needs a name); ignored when one exists — a sale never silently renames a customer. |
| `email` | `string` | No | — |

### `items[]` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `variant_id` | `uuid` | No | The variant to sell. Provide this OR `sku`. |
| `sku` | `string` | No | Resolve the variant by SKU instead of id. |
| `quantity` | `number` | Yes | — |
| `unit_price` | `number` | No | Override the regular unit price (trusted surface). Omitted = the variant's current catalog price — the admin-desk rule, verbatim. |
| `discount_price` | `number` | No | Override the discounted unit price. Only honored when it is a REAL discount (above 0, below the regular price) — same rule as the desk. |

### `payment` fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `payment_method_id` | `uuid` | No | One of the store's payment methods (must be active). |
| `amount` | `number` | Yes | — |
| `transaction_id` | `string` | No | — |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.salafems.com/ext/v1/orders" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "channel": "facebook",
  "customer": {
    "phone": "01712345678",
    "full_name": "Rafiqul Islam"
  },
  "items": [
    {
      "sku": "TSHIRT-RED-M",
      "quantity": 2
    }
  ],
  "shipping_charge": 120,
  "note": "Deliver after 6pm."
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "channel": "facebook",
    "customer": {
      "phone": "01712345678",
      "full_name": "Rafiqul Islam"
    },
    "items": [
      {
        "sku": "TSHIRT-RED-M",
        "quantity": 2
      }
    ],
    "shipping_charge": 120,
    "note": "Deliver after 6pm."
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "channel": "facebook",
    "customer": {
        "phone": "01712345678",
        "full_name": "Rafiqul Islam",
    },
    "items": [
        {
            "sku": "TSHIRT-RED-M",
            "quantity": 2,
        },
    ],
    "shipping_charge": 120,
    "note": "Deliver after 6pm.",
}

response = requests.post(
    "https://api.salafems.com/ext/v1/orders",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'channel' => 'facebook',
    'customer' => [
        'phone' => '01712345678',
        'full_name' => 'Rafiqul Islam',
    ],
    'items' => [
        [
            'sku' => 'TSHIRT-RED-M',
            'quantity' => 2,
        ],
    ],
    'shipping_charge' => 120,
    'note' => 'Deliver after 6pm.',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `201` — The created order, in the same shape as `GET /v1/orders/{id}` with customer and items expanded.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — Missing scope, or the plan order quota is exhausted (`plan_limit_reached`).
- `409` — A request with this idempotency key is still in flight (`IDEMPOTENCY_IN_FLIGHT`).
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "name": "Facebook",
    "slug": "facebook",
    "type": "facebook"
  },
  "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "location_id": null,
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "1250.00",
  "discount_amount": "1250.00",
  "shipping_charge": "1250.00",
  "tax_amount": "1250.00",
  "total_amount": "1250.00",
  "paid_amount": "1250.00",
  "due_amount": "1250.00",
  "refunded_amount": "1250.00",
  "shipping_address": null,
  "coupon": null,
  "custom_fields": null,
  "note": null,
  "delivered_at": "2026-08-11T10:00:00.000Z",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "items": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "quantity": 2,
      "unit_price": "1250.00",
      "discount_price": "1250.00",
      "subtotal": "1250.00"
    }
  ],
  "customer": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "full_name": "Rafiqul Islam",
    "email": null,
    "phone": "01712345678",
    "type": "manual",
    "status": "active"
  }
}
```

## Get an order by id

`GET /ext/v1/orders/{id}` · Requires scope `orders:read`

Add `?expand=customer,items` for the related objects.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The order id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `expand` | `string[]` | No | — |
| `store_id` | `string` | No | Restrict the lookup to one store (company-scoped keys). |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID?expand=customer%2Citems" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID");
Object.entries({
  expand: "customer,items",
}).forEach(([key, value]) => url.searchParams.set(key, value));

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID",
    params={
        "expand": "customer,items",
    },
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$ch = curl_init('https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID?expand=customer%2Citems');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The order.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Order not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "name": "Facebook",
    "slug": "facebook",
    "type": "facebook"
  },
  "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "location_id": null,
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "1250.00",
  "discount_amount": "1250.00",
  "shipping_charge": "1250.00",
  "tax_amount": "1250.00",
  "total_amount": "1250.00",
  "paid_amount": "1250.00",
  "due_amount": "1250.00",
  "refunded_amount": "1250.00",
  "shipping_address": null,
  "coupon": null,
  "custom_fields": null,
  "note": null,
  "delivered_at": "2026-08-11T10:00:00.000Z",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "items": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "quantity": 2,
      "unit_price": "1250.00",
      "discount_price": "1250.00",
      "subtotal": "1250.00"
    }
  ],
  "customer": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "full_name": "Rafiqul Islam",
    "email": null,
    "phone": "01712345678",
    "type": "manual",
    "status": "active"
  }
}
```

## Cancel an order

`POST /ext/v1/orders/{id}/cancel` · Requires scope `orders:write`

Sugar for the `cancelled` transition: same map, same stock unwind (release an open reservation, reverse a committed sale).

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The order id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `note` | `string` | No | Recorded on the order's timeline. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/cancel" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Customer changed their mind."
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/cancel");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "note": "Customer changed their mind."
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "note": "Customer changed their mind.",
}

response = requests.post(
    "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/cancel",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'note' => 'Customer changed their mind.',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/cancel');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The cancelled order (customer and items expanded).
- `400` — The order cannot be cancelled from its current status (`INVALID_STATUS_TRANSITION`).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Order not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "name": "Facebook",
    "slug": "facebook",
    "type": "facebook"
  },
  "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "location_id": null,
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "1250.00",
  "discount_amount": "1250.00",
  "shipping_charge": "1250.00",
  "tax_amount": "1250.00",
  "total_amount": "1250.00",
  "paid_amount": "1250.00",
  "due_amount": "1250.00",
  "refunded_amount": "1250.00",
  "shipping_address": null,
  "coupon": null,
  "custom_fields": null,
  "note": null,
  "delivered_at": "2026-08-11T10:00:00.000Z",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "items": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "quantity": 2,
      "unit_price": "1250.00",
      "discount_price": "1250.00",
      "subtotal": "1250.00"
    }
  ],
  "customer": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "full_name": "Rafiqul Islam",
    "email": null,
    "phone": "01712345678",
    "type": "manual",
    "status": "active"
  }
}
```

## List an order's payments

`GET /ext/v1/orders/{id}/payments` · Requires scope `payments:read`

Every settled, pending and refunded row against the order, oldest first. Refunds are negative amounts with status `refunded`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The order id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Restrict the lookup to one store. Breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments");

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const { data } = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments",
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

page = response.json()
rows = page["data"]
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$ch = curl_init('https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}

$rows = $response['data'];
```

### Responses

- `200` — The order's payment rows.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Order not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "data": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "order_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "amount": "1250.00",
      "status": "paid",
      "transaction_id": "TRX9F3K2",
      "payment_method": {
        "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
        "name": "bKash",
        "type": "mobile_banking"
      },
      "paid_at": "2026-08-11T10:00:00.000Z",
      "created_at": "2026-08-11T10:00:00.000Z"
    }
  ]
}
```

## Record a payment against an order

`POST /ext/v1/orders/{id}/payments` · Requires scope `payments:write`

The staff path verbatim: the amount is capped at the order's NET due (refunds counted), the paid/due rollup recomputes under a row lock, and the paid-in-full events fire exactly once. A provider transaction id is unique per store — recording the same TrxID twice is a 409.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The order id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | Yes | REQUIRED. A unique value per logical request (a UUID is ideal); reuse it verbatim when retrying. A retry replays the original response with `Idempotent-Replayed: true` instead of running twice. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `amount` | `number` | Yes | — |
| `payment_method_id` | `uuid` | No | One of the store's payment methods (must be active). |
| `transaction_id` | `string` | No | Provider transaction id. Unique per store — recording the same TrxID twice is a 409. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": 500,
  "transaction_id": "TRX9F3K2"
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "amount": 500,
    "transaction_id": "TRX9F3K2"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "amount": 500,
    "transaction_id": "TRX9F3K2",
}

response = requests.post(
    "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'amount' => 500,
    'transaction_id' => 'TRX9F3K2',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/payments');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `201` — The updated order (customer and items expanded).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Order not found.
- `409` — Duplicate transaction id, or an idempotency key still in flight.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "name": "Facebook",
    "slug": "facebook",
    "type": "facebook"
  },
  "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "location_id": null,
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "1250.00",
  "discount_amount": "1250.00",
  "shipping_charge": "1250.00",
  "tax_amount": "1250.00",
  "total_amount": "1250.00",
  "paid_amount": "1250.00",
  "due_amount": "1250.00",
  "refunded_amount": "1250.00",
  "shipping_address": null,
  "coupon": null,
  "custom_fields": null,
  "note": null,
  "delivered_at": "2026-08-11T10:00:00.000Z",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "items": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "quantity": 2,
      "unit_price": "1250.00",
      "discount_price": "1250.00",
      "subtotal": "1250.00"
    }
  ],
  "customer": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "full_name": "Rafiqul Islam",
    "email": null,
    "phone": "01712345678",
    "type": "manual",
    "status": "active"
  }
}
```

## Change an order status

`PATCH /ext/v1/orders/{id}/status` · Requires scope `orders:write`

Validated against the SAME strict transition map staff use — an illegal move is refused with `INVALID_STATUS_TRANSITION`. Stock side effects (commit on delivery, release/reverse on cancel/return) are the staff path's, inherited.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The order id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `status` | `string` | Yes | — One of: `pending`, `follow_up`, `confirmed`, `processing`, `ready_to_ship`, `in_transit`, `delivered`, `returned`, `failed_delivery`, `cancelled`, `hold`, `spam`. |
| `note` | `string` | No | Recorded on the order's timeline. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X PATCH "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/status" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "confirmed",
  "note": "Confirmed over the phone."
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/status");

const response = await fetch(url, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "status": "confirmed",
    "note": "Confirmed over the phone."
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "status": "confirmed",
    "note": "Confirmed over the phone.",
}

response = requests.patch(
    "https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/status",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'status' => 'confirmed',
    'note' => 'Confirmed over the phone.',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/orders/REPLACE_WITH_ID/status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The updated order (customer and items expanded).
- `400` — The transition map forbids this move (`INVALID_STATUS_TRANSITION`).
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Order not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "name": "Facebook",
    "slug": "facebook",
    "type": "facebook"
  },
  "customer_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "location_id": null,
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "1250.00",
  "discount_amount": "1250.00",
  "shipping_charge": "1250.00",
  "tax_amount": "1250.00",
  "total_amount": "1250.00",
  "paid_amount": "1250.00",
  "due_amount": "1250.00",
  "refunded_amount": "1250.00",
  "shipping_address": null,
  "coupon": null,
  "custom_fields": null,
  "note": null,
  "delivered_at": "2026-08-11T10:00:00.000Z",
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "items": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "quantity": 2,
      "unit_price": "1250.00",
      "discount_price": "1250.00",
      "subtotal": "1250.00"
    }
  ],
  "customer": {
    "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
    "full_name": "Rafiqul Islam",
    "email": null,
    "phone": "01712345678",
    "type": "manual",
    "status": "active"
  }
}
```
