## List Orders

```
GET /api/v1/orders
```

Returns a paginated list of orders, sorted by creation date (newest first).

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | 10 | Orders per page (1-100) |
| `offset` | `number` | 0 | Orders to skip |

### Response

```json
{
  "data": [
    {
      "id": "0191abc0-1234-7def-8000-000000000001",
      "orderNumber": 1042,
      "status": "completed",
      "totalAmount": "5000",
      "currency": "usd",
      "customer": {
        "id": "0191abc0-0000-7000-8000-000000000010",
        "email": "jane@example.com",
        "name": "Jane Doe"
      },
      "createdAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "meta": {
    "count": 42
  }
}
```

---

## Get Order

```
GET /api/v1/orders/:id
```

Returns the full order with line items, customer info, shipping address, and payment details.

### Response

```json
{
  "id": "0191abc0-1234-7def-8000-000000000001",
  "orderNumber": 1042,
  "status": "completed",
  "totalAmount": "5000",
  "currency": "usd",
  "lineItems": [
    {
      "id": "0191abc0-0000-7000-8000-000000000100",
      "productVariantId": "0191abc0-0000-7000-8000-000000000200",
      "quantity": 2,
      "unitPrice": "2500",
      "product": {
        "name": "Classic Tee",
        "slug": "classic-tee"
      }
    }
  ],
  "customer": {
    "id": "0191abc0-0000-7000-8000-000000000010",
    "email": "jane@example.com",
    "name": "Jane Doe"
  },
  "shippingAddress": {
    "line1": "123 Main St",
    "city": "San Francisco",
    "state": "CA",
    "postalCode": "94102",
    "country": "US"
  },
  "createdAt": "2024-01-15T10:30:00.000Z"
}
```

### Prices and tax

Every money field in an order's `orderData` snapshot is **net**, exactly like the live catalog's. Gross twins are returned alongside them, so a storefront prints a past order the same way it prints a cart:

| Net field | Gross twin |
|-----------|------------|
| `orderData.lineItems[].productVariant.price` | `…productVariant.priceGross` |
| `orderData.shipping.price` | `orderData.shipping.priceGross` |

The line variants carry the full set of twins described in [Prices and tax](/docs/api-reference/products#prices-and-tax) (`originalPriceGross`, `calculatedPriceGross`, and so on). They are computed from the tax rate **frozen into the snapshot** — each line from its own product's rate, delivery from the shipping method's — never from the product's current rate, which may have changed since. `shipping.priceGross` is the listed delivery charge; whether the order actually earned free delivery is already settled in the order's totals.

Both `GET /api/v1/orders` and `GET /api/v1/orders/:id` return them.

---

## Update Order

```
PATCH /api/v1/orders/:id
```

Updates an order's fulfillment status and/or tracking information.

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `status` | `string` | `processing`, `shipped`, `delivered`, or `canceled` |
| `trackingNumber` | `string` | Shipping carrier tracking number. Only for orders fulfilled outside the platform – orders whose shipping method links a carrier addon (`gls`, `inpost`) are rejected with `409`. Use the [Shipment API](#create-shipment) for carrier-managed orders. |

```bash
curl -X PATCH \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "shipped",
    "trackingNumber": "1Z999AA10123456784"
  }' \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001
```

### Customer Notifications

When a status change results in a real transition (e.g. `processing` → `shipped`), the endpoint automatically sends the matching customer notification email – the same one the admin dashboard triggers. This only fires when the store has the relevant email template enabled at `/manage/emails`. Retrying the same status update does not re-send the notification.

### Order Statuses

| API Status | Internal Status | Description |
|------------|----------------|-------------|
| `processing` | processing | Being prepared |
| `shipped` | shipped | In transit |
| `delivered` | completed | Delivered to customer |
| `canceled` | cancelled | Cancelled |

---

## Create Shipment

```
POST /api/v1/orders/:id/shipment
```

Creates a carrier shipment for the order. The carrier is resolved from the order's shipping method (its linked shipping addon) – you don't specify the carrier in the request. The store's environment (`live`/`test`) determines whether the carrier's sandbox or production API is used.

### Request Body

All fields are optional. The request body can be empty – every field has a server-side default or returns a `422` explaining what's missing.

#### InPost Fields

| Field | Type | Description |
|-------|------|-------------|
| `template` | `string` | Locker size: `small`, `medium`, or `large` (maps to gabaryt A/B/C). Omit to use dimension-based suggestion, then the store's `fallbackTemplate`. Fails with `422` if neither is available. |
| `targetPoint` | `string` | InPost locker code (e.g. `KRA012`). Defaults to the buyer's checkout selection. |
| `sendingMethod` | `string` | How the parcel is handed to InPost: `parcel_locker` (drop-off, default) or `dispatch_order` (courier pickup). |

#### GLS Fields

| Field | Type | Description |
|-------|------|-------------|
| `weightKg` | `number` | Parcel weight in kg (0.01–50). Defaults to the sum of the order's shippable variant weights. |
| `date` | `string` | Pickup/drop date (`YYYY-MM-DD`). Defaults to the next working day. |
| `pointId` | `string` | GLS parcel-shop id (e.g. `GLS_PL-12345`). Defaults to the buyer's checkout selection; required for parcel-shop rates. |

Passing fields that belong to the other carrier returns `400` – e.g. sending `weightKg` for an InPost order is rejected.

```bash
# InPost shipment
curl -X POST \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"template": "medium"}' \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/shipment

# GLS shipment (empty body – uses defaults)
curl -X POST \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{}' \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/shipment
```

### Response (201)

InPost:

```json
{
  "carrier": "inpost",
  "shipmentId": "123456789",
  "status": "created",
  "trackingNumber": null,
  "labelReady": false,
  "template": "medium",
  "templateSource": "explicit"
}
```

GLS:

```json
{
  "carrier": "gls",
  "shipmentId": "987654321",
  "parcelNumbers": ["12345678901"],
  "trackId": "T12345",
  "trackingUrl": "https://gls-group.eu/track/T12345",
  "labelReady": true,
  "labelWarning": null
}
```

### Error Responses

| Status | Description |
|--------|-------------|
| `400` | Invalid request data or carrier-mismatched fields |
| `404` | Order not found |
| `409` | Order already has a shipment, or shipping method has no supported carrier |
| `422` | Missing required carrier data (e.g. no locker code or template for InPost) |
| `502` | Carrier API error |

---

## Get Shipment

```
GET /api/v1/orders/:id/shipment
```

Returns the current shipment status for an order.

### Response (200)

InPost:

```json
{
  "carrier": "inpost",
  "shipmentId": "123456789",
  "status": "confirmed",
  "trackingNumber": "620123456789012345678901",
  "trackingUrl": "https://inpost.pl/sledzenie-przesylek?number=620123456789012345678901",
  "labelReady": true
}
```

GLS:

```json
{
  "carrier": "gls",
  "shipmentId": "987654321",
  "parcelNumbers": ["12345678901"],
  "trackId": "T12345",
  "trackingUrl": "https://gls-group.eu/track/T12345",
  "labelReady": true
}
```

### Error Responses

| Status | Description |
|--------|-------------|
| `404` | Order not found or order has no shipment |
| `409` | Shipping method has no supported carrier |
| `502` | Carrier API error (InPost only – GLS uses locally persisted state) |

---

## Delete Shipment

```
DELETE /api/v1/orders/:id/shipment
```

Deletes the shipment for an order. Only supported for GLS – InPost shipments must be cancelled in the InPost Parcel Manager.

```bash
curl -X DELETE \
  -H "Authorization: Bearer your_api_key" \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/shipment
```

### Response (200)

```json
{
  "ok": true,
  "deleted": 1
}
```

### Error Responses

| Status | Description |
|--------|-------------|
| `404` | Order not found or order has no shipment |
| `409` | InPost shipments cannot be deleted through this API, or the GLS shipment is no longer deletable |
| `502` | GLS API error |

---

## Get Shipment Label

```
GET /api/v1/orders/:id/shipment/label
```

Downloads the shipping label as a file. Returns the raw file (PDF, ZPL, or EPL) with appropriate `Content-Type` and `Content-Disposition` headers.

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `format` | `string` | `pdf` | InPost only: `pdf`, `zpl`, or `epl`. GLS uses the store-wide `labelMode` setting (change via `PUT /api/v1/addons/gls`). Passing `format` for a GLS order returns `400`. |

```bash
# Download PDF label
curl -H "Authorization: Bearer your_api_key" \
  -o label.pdf \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/shipment/label

# Download ZPL for thermal printer (InPost only)
curl -H "Authorization: Bearer your_api_key" \
  -o label.zpl \
  "https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/shipment/label?format=zpl"
```

### Error Responses

| Status | Description |
|--------|-------------|
| `400` | Invalid query parameters, or `format` used with GLS |
| `404` | Order not found or order has no shipment |
| `409` | InPost label not ready yet (poll `GET .../shipment` until `labelReady` is `true`), or shipping method has no supported carrier |
| `502` | Carrier API error |

---

## Customer Orders

```
GET /api/v1/customers/:id/orders
```

Returns orders for a specific customer, paginated.

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | 20 | Orders per page (1-100) |
| `offset` | `number` | 0 | Orders to skip |

### Response

```json
{
  "items": [
    {
      "id": "0191abc0-1234-7def-8000-000000000001",
      "orderNumber": "1042",
      "status": "completed",
      "totalCents": 5000,
      "currency": "usd",
      "createdAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 8,
    "offset": 0,
    "limit": 20,
    "hasMore": false
  }
}
```

---

## List Refunds

```
GET /api/v1/orders/:id/refunds
```

Returns the refunds recorded against an order, newest first.

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | 50 | Refunds per page (1-100) |
| `offset` | `number` | 0 | Refunds to skip |

```bash
curl \
  -H "Authorization: Bearer your_api_key" \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/refunds
```

### Response

```json
{
  "data": [
    {
      "id": "0191abc0-1234-7def-8000-00000000000a",
      "orderId": "0191abc0-1234-7def-8000-000000000001",
      "stripeRefundId": "re_3Abc123",
      "amount": 2500,
      "currency": "usd",
      "reason": "requested_by_customer",
      "reasonNote": "Customer changed their mind",
      "status": "succeeded",
      "refundType": "partial",
      "refundApplicationFee": false,
      "restockItems": true,
      "shippingAmount": null,
      "createdAt": "2024-05-02T10:00:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "offset": 0,
    "limit": 50
  }
}
```

---

## Create Refund

```
POST /api/v1/orders/:id/refunds
```

Issues a refund against the order's original payment. The order's own payment provider decides how it is processed: PayNow orders refund through PayNow, everything else through Stripe. Providers this API cannot refund on your behalf are refused with a `400` – see [Payment Providers](#payment-providers) below.

### Body Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | `number` | Yes | Refund amount in cents (smallest currency unit) |
| `reason` | `string` | Yes | One of `requested_by_customer`, `damaged_or_defective`, `duplicate`, `fraudulent`, `order_error`, `other` |
| `reasonNote` | `string` | No | Additional detail about the reason |
| `refundType` | `string` | Yes | `full` or `partial` |
| `refundApplicationFee` | `boolean` | No | Refund the Stripe application fee. Defaults to `false`. |
| `restockItems` | `boolean` | No | Return refunded items to stock. Defaults to `true`. |
| `lineItems` | `array` | No | Per-item breakdown for partial refunds with item tracking |
| `shippingAmount` | `number` | No | Portion of `amount` that refunds shipping, in cents. Must not exceed `amount`. |

Each `lineItems` entry takes `lineItemIndex` (position in the order), `productVariantId`, `quantity`, and `amount` in cents.

Restocking is bounded by the order itself. A line is restocked up to the quantity the order contains for that variant – asking for more restocks only the ordered quantity – and a line naming a variant the order does not contain is ignored. Ignored lines do not fail the request: the refund is already recorded and the money has moved.

```bash
curl -X POST \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2500,
    "reason": "requested_by_customer",
    "refundType": "partial",
    "restockItems": true
  }' \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/refunds
```

### Payment Providers

| Provider | Behaviour |
|----------|-----------|
| Stripe | Refunded through Stripe against the original charge |
| PayNow | Refunded through PayNow; the refund row stays `pending` until PayNow's settlement webhook lands |
| Frame | `400` – refund it in the Frame dashboard, then set the order status to `Refunded` |
| Bank transfer | `400` – refund it manually from your bank |

### Errors

| Status | Meaning |
|--------|---------|
| `400` | Order status can't be refunded, the payment provider can't be refunded through this API, no Stripe payment found, the amount exceeds what's still refundable, or `shippingAmount` exceeds `amount` |
| `404` | Order not found |

Only orders with status `paid`, `processing`, `shipped`, `completed`, or `partially_refunded` can be refunded.

Remaining headroom counts **committed** refunds – both `succeeded` and `pending` rows. A refund still awaiting its provider's settlement webhook already consumes headroom, so `amount` plus every committed refund on the order may not exceed the order total.

---

## Get Refund

```
GET /api/v1/orders/:id/refunds/:refundId
```

Returns a single refund belonging to the order.

```bash
curl \
  -H "Authorization: Bearer your_api_key" \
  https://your-store.yns.store/api/v1/orders/0191abc0-1234-7def-8000-000000000001/refunds/0191abc0-1234-7def-8000-00000000000a
```