# Orange Wine — API Part 10: Catalog Administration

Version 1.0 — 28 Sep 2026 · 56 endpoints · Base URL `/api/v1` · Read with Part 00 (conventions, error catalog, permissions).

## 1. Before you start

- Base URL `/api/v1` (webhooks: `/webhooks/...`). JSON, `snake_case` keys, prefixed string IDs, ISO 8601 UTC timestamps.
- Success: `{ data, request_id, correlation_id }`. Error: `{ error: { code, message, details, retryable } }`. Gated feature: `data.enabled = false`, `status = "pending_configuration"`.
- Auth via HttpOnly cookies (`access_token`, `refresh_token`); every non-GET browser request sends `X-XSRF-TOKEN`.
- Money is always `{ amount_minor, currency }`. Percentages are basis points.
- Commands marked Idempotency = Required need an `Idempotency-Key` header (UUID v4).
- Status never changes through PATCH — use the named command endpoints.
- Full conventions, error catalog, permission catalog and open items: Part 00.

### Error codes used in this part

| Code | HTTP | Meaning |
|---|---|---|
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

### Permissions used in this part

| Permission | Grants | Endpoints |
|---|---|---|
| `catalog.manage` | Products, taxonomy, media, content, price books | 56 |

### PROPOSED and gated endpoints in this part

| Status | Method | Path |
|---|---|---|
| PROPOSED | GET | `/dashboard/product-groups` |
| PROPOSED | POST | `/dashboard/product-groups` |
| PROPOSED | PATCH | `/dashboard/product-groups/{groupId}` |
| PROPOSED | DELETE | `/dashboard/product-groups/{groupId}` |

## 2. Catalog Administration

Flat products only (D-09). No endpoint accepts `product_variant_id`/`variant_id`. State changes use named commands.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/dashboard/products` | Product list | CONFIRMED |
| 2.2 | POST | `/dashboard/products` | Create product | CONFIRMED |
| 2.3 | GET | `/dashboard/products/{productId}` | Product detail (admin) | CONFIRMED |
| 2.4 | PATCH | `/dashboard/products/{productId}` | Update product | CONFIRMED |
| 2.5 | POST | `/dashboard/products/{productId}/publish` | Publish | CONFIRMED |
| 2.6 | POST | `/dashboard/products/{productId}/unpublish` | Unpublish | CONFIRMED |
| 2.7 | POST | `/dashboard/products/{productId}/archive` | Archive | CONFIRMED |
| 2.8 | GET | `/dashboard/products/{productId}/barcodes` | Barcodes | CONFIRMED |
| 2.9 | POST | `/dashboard/products/{productId}/barcodes` | Add barcode | CONFIRMED |
| 2.10 | DELETE | `/dashboard/products/{productId}/barcodes/{barcodeId}` | Remove barcode | CONFIRMED |
| 2.11 | GET | `/dashboard/product-groups` | Product groups | PROPOSED |
| 2.12 | POST | `/dashboard/product-groups` | Create product group | PROPOSED |
| 2.13 | PATCH | `/dashboard/product-groups/{groupId}` | Update product group | PROPOSED |
| 2.14 | DELETE | `/dashboard/product-groups/{groupId}` | Delete product group | PROPOSED |
| 2.15 | GET | `/dashboard/departments` | List departments | CONFIRMED |
| 2.16 | POST | `/dashboard/departments` | Create department | CONFIRMED |
| 2.17 | PATCH | `/dashboard/departments/{id}` | Update department | CONFIRMED |
| 2.18 | DELETE | `/dashboard/departments/{id}` | Delete department | CONFIRMED |
| 2.19 | GET | `/dashboard/categories` | List categories | CONFIRMED |
| 2.20 | POST | `/dashboard/categories` | Create categorie | CONFIRMED |
| 2.21 | PATCH | `/dashboard/categories/{id}` | Update categorie | CONFIRMED |
| 2.22 | DELETE | `/dashboard/categories/{id}` | Delete categorie | CONFIRMED |
| 2.23 | GET | `/dashboard/collections` | List collections | CONFIRMED |
| 2.24 | POST | `/dashboard/collections` | Create collection | CONFIRMED |
| 2.25 | PATCH | `/dashboard/collections/{id}` | Update collection | CONFIRMED |
| 2.26 | DELETE | `/dashboard/collections/{id}` | Delete collection | CONFIRMED |
| 2.27 | GET | `/dashboard/brands` | List brands | CONFIRMED |
| 2.28 | POST | `/dashboard/brands` | Create brand | CONFIRMED |
| 2.29 | PATCH | `/dashboard/brands/{id}` | Update brand | CONFIRMED |
| 2.30 | DELETE | `/dashboard/brands/{id}` | Delete brand | CONFIRMED |
| 2.31 | GET | `/dashboard/attributes` | List attributes | CONFIRMED |
| 2.32 | POST | `/dashboard/attributes` | Create attribute | CONFIRMED |
| 2.33 | PATCH | `/dashboard/attributes/{id}` | Update attribute | CONFIRMED |
| 2.34 | DELETE | `/dashboard/attributes/{id}` | Delete attribute | CONFIRMED |
| 2.35 | GET | `/dashboard/attribute-values` | Attribute values | CONFIRMED |
| 2.36 | POST | `/dashboard/attribute-values` | Add attribute value | CONFIRMED |
| 2.37 | GET | `/dashboard/media` | Media library | CONFIRMED |
| 2.38 | POST | `/dashboard/media` | Upload media | CONFIRMED |
| 2.39 | PATCH | `/dashboard/media/{mediaId}` | Update media | CONFIRMED |
| 2.40 | DELETE | `/dashboard/media/{mediaId}` | Delete media | CONFIRMED |
| 2.41 | GET | `/dashboard/seo-pages` | List seo-pages | CONFIRMED |
| 2.42 | POST | `/dashboard/seo-pages` | Create in seo-pages | CONFIRMED |
| 2.43 | PATCH | `/dashboard/seo-pages/{id}` | Update in seo-pages | CONFIRMED |
| 2.44 | DELETE | `/dashboard/seo-pages/{id}` | Delete in seo-pages | CONFIRMED |
| 2.45 | GET | `/dashboard/pages` | List pages | CONFIRMED |
| 2.46 | POST | `/dashboard/pages` | Create in pages | CONFIRMED |
| 2.47 | PATCH | `/dashboard/pages/{id}` | Update in pages | CONFIRMED |
| 2.48 | DELETE | `/dashboard/pages/{id}` | Delete in pages | CONFIRMED |
| 2.49 | GET | `/dashboard/blog/posts` | List blog/posts | CONFIRMED |
| 2.50 | POST | `/dashboard/blog/posts` | Create in blog/posts | CONFIRMED |
| 2.51 | PATCH | `/dashboard/blog/posts/{id}` | Update in blog/posts | CONFIRMED |
| 2.52 | DELETE | `/dashboard/blog/posts/{id}` | Delete in blog/posts | CONFIRMED |
| 2.53 | GET | `/dashboard/menus` | List menus | CONFIRMED |
| 2.54 | POST | `/dashboard/menus` | Create in menus | CONFIRMED |
| 2.55 | PATCH | `/dashboard/menus/{id}` | Update in menus | CONFIRMED |
| 2.56 | DELETE | `/dashboard/menus/{id}` | Delete in menus | CONFIRMED |

- Content endpoints (seo-pages, pages, blog/posts, menus) share one shape; menus use `{ code, items[] }` instead of `body_html`.

### 2.1 `GET /dashboard/products`

**Product list**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/products` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| q | string · optional | Name/SKU/barcode. |
| status | enum · optional |  |
| category_id / brand_id | string · optional |  |
| pre_arrival / rapid_ship_eligible / ship_free_12_eligible | boolean · optional |  |
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/products?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "prod_101",
        "name": "Example Cabernet 2023 750ml",
        "slug": "example-cabernet-2023-750ml",
        "sku": "CAB-2023-750",
        "status": "published",
        "sell_unit": "bottle",
        "pack_quantity": 1,
        "product_group_id": "pg_20",
        "department_id": "dept_wine",
        "category_id": "cat_red",
        "brand_id": "brand_7",
        "tax_class": "wine",
        "age_restricted": true,
        "price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "cost": {
          "amount_minor": 3500,
          "currency": "USD"
        },
        "weight": {
          "value": 1.35,
          "unit": "lb"
        },
        "dimensions": {
          "length": 4,
          "width": 4,
          "height": 13,
          "unit": "in"
        },
        "shippable": true,
        "flags": {
          "ship_free_12_eligible": true,
          "rapid_ship_eligible": true,
          "pre_arrival": false
        },
        "expected_lead_time_days": null,
        "supplier_reference": null,
        "barcodes": [
          "012345678905"
        ],
        "attributes": {
          "country": "USA",
          "varietal": "Cabernet Sauvignon",
          "vintage": 2023,
          "size": "750ml",
          "abv": 14.5
        },
        "created_at": "2026-09-01T10:00:00Z",
        "updated_at": "2026-09-10T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| pagination.page | integer | Current page. |
| pagination.per_page | integer | Page size used. |
| pagination.total | integer | Total matching items. |
| pagination.last_page | integer | Last page number; 0 when there are no items. |

---

### 2.2 `POST /dashboard/products`

**Create product**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/products` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/products HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Example Cabernet 2023 750ml",
  "slug": "example-cabernet-2023-750ml",
  "sku": "CAB-2023-750",
  "sell_unit": "bottle",
  "pack_quantity": 1,
  "product_group_id": "pg_20",
  "department_id": "dept_wine",
  "category_id": "cat_red",
  "brand_id": "brand_7",
  "tax_class": "wine",
  "age_restricted": true,
  "price": {
    "amount_minor": 6275,
    "currency": "USD"
  },
  "cost": {
    "amount_minor": 3500,
    "currency": "USD"
  },
  "weight": {
    "value": 1.35,
    "unit": "lb"
  },
  "dimensions": {
    "length": 4,
    "width": 4,
    "height": 13,
    "unit": "in"
  },
  "shippable": true,
  "flags": {
    "ship_free_12_eligible": true,
    "rapid_ship_eligible": true,
    "pre_arrival": false
  },
  "expected_lead_time_days": null,
  "supplier_reference": null,
  "barcodes": [
    "012345678905"
  ],
  "attributes": {
    "country": "USA",
    "varietal": "Cabernet Sauvignon",
    "vintage": 2023,
    "size": "750ml",
    "abv": 14.5
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name / slug / sku | string · required (slug auto if omitted) | SKU unique. |
| status | enum · response only | `draft` \| `published` \| `unpublished` \| `archived` — changed only by commands. |
| sell_unit | enum · required | `bottle` \| `case` \| `pack` \| `other`. |
| pack_quantity | integer · required | Descriptive only. |
| product_group_id | string \| null · optional | Display grouping only (D-09). |
| tax_class | enum · required | (PROPOSED) `wine` \| `spirits` \| `beer` \| `non_alcoholic` \| `merchandise` — final set with tax provider (D-32). |
| price / cost | Money object `{ amount_minor: integer, currency: "USD" }` | Base price (price books can override). |
| weight / dimensions | object · required to publish if shippable |  |
| flags.ship_free_12_eligible / rapid_ship_eligible | boolean | Changing these also requires `promotions.manage`. |
| flags.pre_arrival | boolean | If `true`: `expected_lead_time_days` required and `rapid_ship_eligible` must be `false`. |
| expected_lead_time_days | integer \| null | Pre-arrival only. |
| supplier_reference | string \| null |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "draft",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | New products start `draft`. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate SKU; pre-arrival + rapid ship; missing lead time |
| `FORBIDDEN` | 403 | Flag change without promotions.manage |

---

### 2.3 `GET /dashboard/products/{productId}`

**Product detail (admin)**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/products/{productId}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
GET /api/v1/dashboard/products/{productId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "published",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name / slug / sku | string · required (slug auto if omitted) | SKU unique. |
| status | enum · response only | `draft` \| `published` \| `unpublished` \| `archived` — changed only by commands. |
| sell_unit | enum · required | `bottle` \| `case` \| `pack` \| `other`. |
| pack_quantity | integer · required | Descriptive only. |
| product_group_id | string \| null · optional | Display grouping only (D-09). |
| tax_class | enum · required | (PROPOSED) `wine` \| `spirits` \| `beer` \| `non_alcoholic` \| `merchandise` — final set with tax provider (D-32). |
| price / cost | Money object `{ amount_minor: integer, currency: "USD" }` | Base price (price books can override). |
| weight / dimensions | object · required to publish if shippable |  |
| flags.ship_free_12_eligible / rapid_ship_eligible | boolean | Changing these also requires `promotions.manage`. |
| flags.pre_arrival | boolean | If `true`: `expected_lead_time_days` required and `rapid_ship_eligible` must be `false`. |
| expected_lead_time_days | integer \| null | Pre-arrival only. |
| supplier_reference | string \| null |  |

---

### 2.4 `PATCH /dashboard/products/{productId}`

**Update product**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/products/{productId}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/products/{productId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "price": {
    "amount_minor": 5999,
    "currency": "USD"
  },
  "flags": {
    "rapid_ship_eligible": false
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any create field except status) | optional |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "published",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |

---

### 2.5 `POST /dashboard/products/{productId}/publish`

**Publish**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/products/{productId}/publish` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
POST /api/v1/dashboard/products/{productId}/publish HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "published",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z",
    "published_at": "2026-09-17T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Shippable product missing weight/dimensions |

**Error example — VALIDATION_ERROR**

```http
HTTP/1.1 422 Unprocessable Entity

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The product cannot be published.",
    "details": {
      "fields": {
        "weight": [
          "Weight is required for shippable products."
        ]
      }
    },
    "retryable": false
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.6 `POST /dashboard/products/{productId}/unpublish`

**Unpublish**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/products/{productId}/unpublish` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
POST /api/v1/dashboard/products/{productId}/unpublish HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "unpublished",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.7 `POST /dashboard/products/{productId}/archive`

**Archive**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/products/{productId}/archive` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
POST /api/v1/dashboard/products/{productId}/archive HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml",
    "slug": "example-cabernet-2023-750ml",
    "sku": "CAB-2023-750",
    "status": "archived",
    "sell_unit": "bottle",
    "pack_quantity": 1,
    "product_group_id": "pg_20",
    "department_id": "dept_wine",
    "category_id": "cat_red",
    "brand_id": "brand_7",
    "tax_class": "wine",
    "age_restricted": true,
    "price": {
      "amount_minor": 6275,
      "currency": "USD"
    },
    "cost": {
      "amount_minor": 3500,
      "currency": "USD"
    },
    "weight": {
      "value": 1.35,
      "unit": "lb"
    },
    "dimensions": {
      "length": 4,
      "width": 4,
      "height": 13,
      "unit": "in"
    },
    "shippable": true,
    "flags": {
      "ship_free_12_eligible": true,
      "rapid_ship_eligible": true,
      "pre_arrival": false
    },
    "expected_lead_time_days": null,
    "supplier_reference": null,
    "barcodes": [
      "012345678905"
    ],
    "attributes": {
      "country": "USA",
      "varietal": "Cabernet Sauvignon",
      "vintage": 2023,
      "size": "750ml",
      "abv": 14.5
    },
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-10T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Notes**

- Historical orders keep their snapshots; archived products cannot be added to carts.

---

### 2.8 `GET /dashboard/products/{productId}/barcodes`

**Barcodes**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/products/{productId}/barcodes` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
GET /api/v1/dashboard/products/{productId}/barcodes HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "bc_1",
        "barcode": "012345678905",
        "type": "upc_a"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].type | enum | `upc_a` \| `ean_13` \| `internal` (PROPOSED set). |

---

### 2.9 `POST /dashboard/products/{productId}/barcodes`

**Add barcode**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/products/{productId}/barcodes` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |

**Request example**

```http
POST /api/v1/dashboard/products/{productId}/barcodes HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "barcode": "012345678912",
  "type": "upc_a"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| barcode | string · required | Globally unique. |
| type | enum · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "bc_2",
    "barcode": "012345678912",
    "type": "upc_a"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Barcode belongs to another product |

---

### 2.10 `DELETE /dashboard/products/{productId}/barcodes/{barcodeId}`

**Remove barcode**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/products/{productId}/barcodes/{barcodeId}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| productId | string |  |
| barcodeId | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/products/{productId}/barcodes/{barcodeId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "bc_2",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.11 `GET /dashboard/product-groups`

**Product groups**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/product-groups` |
| Auth | Staff `catalog.manage` |
| Status | PROPOSED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/product-groups HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "pg_20",
        "name": "Example Cabernet 2023",
        "product_ids": [
          "prod_101",
          "prod_102"
        ]
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.12 `POST /dashboard/product-groups`

**Create product group**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/product-groups` |
| Auth | Staff `catalog.manage` |
| Status | PROPOSED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/product-groups HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Example Cabernet 2023",
  "product_ids": [
    "prod_101",
    "prod_102"
  ]
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| product_ids[] | string[] · optional | Display grouping only. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "pg_20",
    "name": "Example Cabernet 2023",
    "product_ids": [
      "prod_101",
      "prod_102"
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.13 `PATCH /dashboard/product-groups/{groupId}`

**Update product group**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/product-groups/{groupId}` |
| Auth | Staff `catalog.manage` |
| Status | PROPOSED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| groupId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/product-groups/{groupId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "product_ids": [
    "prod_101",
    "prod_102",
    "prod_103"
  ]
}
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pg_20",
    "product_ids": [
      "prod_101",
      "prod_102",
      "prod_103"
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.14 `DELETE /dashboard/product-groups/{groupId}`

**Delete product group**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/product-groups/{groupId}` |
| Auth | Staff `catalog.manage` |
| Status | PROPOSED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| groupId | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/product-groups/{groupId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pg_20",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.15 `GET /dashboard/departments`

**List departments**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/departments` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/departments?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "dept_wine",
        "name": "Wine",
        "slug": "wine",
        "description": ""
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.16 `POST /dashboard/departments`

**Create department**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/departments` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/departments HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Wine",
  "slug": "wine",
  "description": ""
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| slug | string · optional | Auto from name. |
| parent_id | string \| null · optional | Categories only. |
| description | string · optional |  |
| seo | object · optional | `{ title, description }`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "dept_wine",
    "name": "Wine",
    "slug": "wine",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate slug |

---

### 2.17 `PATCH /dashboard/departments/{id}`

**Update department**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/departments/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/departments/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Wine"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field) | optional | Same as create. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "dept_wine",
    "name": "Wine",
    "slug": "wine",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.18 `DELETE /dashboard/departments/{id}`

**Delete department**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/departments/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/departments/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "dept_wine",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Still referenced by products |

---

### 2.19 `GET /dashboard/categories`

**List categories**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/categories` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/categories?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "cat_red",
        "name": "Red Wine",
        "slug": "red-wine",
        "parent_id": null,
        "description": ""
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.20 `POST /dashboard/categories`

**Create categorie**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/categories` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/categories HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Red Wine",
  "slug": "red-wine",
  "parent_id": null,
  "description": ""
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| slug | string · optional | Auto from name. |
| parent_id | string \| null · optional | Categories only. |
| description | string · optional |  |
| seo | object · optional | `{ title, description }`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "cat_red",
    "name": "Red Wine",
    "slug": "red-wine",
    "parent_id": null,
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate slug |

---

### 2.21 `PATCH /dashboard/categories/{id}`

**Update categorie**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/categories/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/categories/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Red Wine"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field) | optional | Same as create. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "cat_red",
    "name": "Red Wine",
    "slug": "red-wine",
    "parent_id": null,
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.22 `DELETE /dashboard/categories/{id}`

**Delete categorie**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/categories/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/categories/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "cat_red",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Still referenced by products |

---

### 2.23 `GET /dashboard/collections`

**List collections**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/collections` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/collections?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "col_12sf",
        "name": "12 Ship Free Picks",
        "slug": "12-ship-free-picks",
        "description": ""
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.24 `POST /dashboard/collections`

**Create collection**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/collections` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/collections HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "12 Ship Free Picks",
  "slug": "12-ship-free-picks",
  "description": ""
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| slug | string · optional | Auto from name. |
| parent_id | string \| null · optional | Categories only. |
| description | string · optional |  |
| seo | object · optional | `{ title, description }`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "col_12sf",
    "name": "12 Ship Free Picks",
    "slug": "12-ship-free-picks",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate slug |

---

### 2.25 `PATCH /dashboard/collections/{id}`

**Update collection**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/collections/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/collections/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "12 Ship Free Picks"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field) | optional | Same as create. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "col_12sf",
    "name": "12 Ship Free Picks",
    "slug": "12-ship-free-picks",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.26 `DELETE /dashboard/collections/{id}`

**Delete collection**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/collections/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/collections/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "col_12sf",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Still referenced by products |

---

### 2.27 `GET /dashboard/brands`

**List brands**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/brands` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/brands?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "brand_7",
        "name": "Example Estate",
        "slug": "example-estate",
        "description": ""
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.28 `POST /dashboard/brands`

**Create brand**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/brands` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/brands HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Example Estate",
  "slug": "example-estate",
  "description": ""
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| slug | string · optional | Auto from name. |
| parent_id | string \| null · optional | Categories only. |
| description | string · optional |  |
| seo | object · optional | `{ title, description }`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "brand_7",
    "name": "Example Estate",
    "slug": "example-estate",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate slug |

---

### 2.29 `PATCH /dashboard/brands/{id}`

**Update brand**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/brands/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/brands/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Example Estate"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field) | optional | Same as create. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "brand_7",
    "name": "Example Estate",
    "slug": "example-estate",
    "description": ""
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.30 `DELETE /dashboard/brands/{id}`

**Delete brand**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/brands/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/brands/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "brand_7",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Still referenced by products |

---

### 2.31 `GET /dashboard/attributes`

**List attributes**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/attributes` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/attributes?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "attr_varietal",
        "name": "Varietal",
        "slug": "varietal",
        "type": "select",
        "filterable": true
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.32 `POST /dashboard/attributes`

**Create attribute**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/attributes` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/attributes HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Varietal",
  "slug": "varietal",
  "type": "select",
  "filterable": true
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| slug | string · optional | Auto from name. |
| parent_id | string \| null · optional | Categories only. |
| description | string · optional |  |
| seo | object · optional | `{ title, description }`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "attr_varietal",
    "name": "Varietal",
    "slug": "varietal",
    "type": "select",
    "filterable": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Duplicate slug |

---

### 2.33 `PATCH /dashboard/attributes/{id}`

**Update attribute**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/attributes/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/attributes/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "Varietal"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field) | optional | Same as create. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "attr_varietal",
    "name": "Varietal",
    "slug": "varietal",
    "type": "select",
    "filterable": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.34 `DELETE /dashboard/attributes/{id}`

**Delete attribute**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/attributes/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/attributes/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "attr_varietal",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Still referenced by products |

---

### 2.35 `GET /dashboard/attribute-values`

**Attribute values**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/attribute-values` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| attribute_id | string · required |  |

**Request example**

```http
GET /api/v1/dashboard/attribute-values HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "av_1",
        "attribute_id": "attr_varietal",
        "value": "cabernet-sauvignon",
        "label": "Cabernet Sauvignon"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.36 `POST /dashboard/attribute-values`

**Add attribute value**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/attribute-values` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/attribute-values HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "attribute_id": "attr_varietal",
  "label": "Merlot"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| attribute_id | string · required |  |
| label | string · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "av_2",
    "attribute_id": "attr_varietal",
    "value": "merlot",
    "label": "Merlot"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.37 `GET /dashboard/media`

**Media library**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/media` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/media?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "med_1",
        "url": "https://cdn.example/cab.jpg",
        "alt": "Bottle front",
        "content_type": "image/jpeg"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.38 `POST /dashboard/media`

**Upload media**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/media` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/media HTTP/1.1
Content-Type: multipart/form-data; boundary=...
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "file": "(binary)",
  "alt": "Bottle front",
  "product_id": "prod_101",
  "position": 1
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| file | file · required | Image. |
| alt | string · required | Accessibility text. |
| product_id | string · optional | Attach to product. |
| position | integer · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "med_2",
    "url": "https://cdn.example/cab2.jpg"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.39 `PATCH /dashboard/media/{mediaId}`

**Update media**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/media/{mediaId}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| mediaId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/media/{mediaId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "alt": "Bottle back",
  "position": 2
}
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "med_2",
    "alt": "Bottle back"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.40 `DELETE /dashboard/media/{mediaId}`

**Delete media**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/media/{mediaId}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| mediaId | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/media/{mediaId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "med_2",
    "deleted": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.41 `GET /dashboard/seo-pages`

**List seo-pages**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/seo-pages` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/seo-pages HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.42 `POST /dashboard/seo-pages`

**Create in seo-pages**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/seo-pages` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/seo-pages HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.43 `PATCH /dashboard/seo-pages/{id}`

**Update in seo-pages**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/seo-pages/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/seo-pages/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.44 `DELETE /dashboard/seo-pages/{id}`

**Delete in seo-pages**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/seo-pages/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/seo-pages/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.45 `GET /dashboard/pages`

**List pages**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/pages` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/pages HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.46 `POST /dashboard/pages`

**Create in pages**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/pages` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/pages HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.47 `PATCH /dashboard/pages/{id}`

**Update in pages**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/pages/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/pages/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.48 `DELETE /dashboard/pages/{id}`

**Delete in pages**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/pages/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/pages/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.49 `GET /dashboard/blog/posts`

**List blog/posts**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/blog/posts` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/blog/posts HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.50 `POST /dashboard/blog/posts`

**Create in blog/posts**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/blog/posts` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/blog/posts HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.51 `PATCH /dashboard/blog/posts/{id}`

**Update in blog/posts**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/blog/posts/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/blog/posts/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.52 `DELETE /dashboard/blog/posts/{id}`

**Delete in blog/posts**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/blog/posts/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/blog/posts/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.53 `GET /dashboard/menus`

**List menus**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/menus` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/menus HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.54 `POST /dashboard/menus`

**Create in menus**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/menus` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/menus HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.55 `PATCH /dashboard/menus/{id}`

**Update in menus**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/menus/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/menus/{id} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "slug": "returns",
  "title": "Returns Policy",
  "body_html": "<p>...</p>",
  "status": "published"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| slug | string · required |  |
| title | string · required |  |
| body_html | string · required | Sanitized server-side. |
| status | enum · optional | `draft` \| `published`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.56 `DELETE /dashboard/menus/{id}`

**Delete in menus**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/menus/{id}` |
| Auth | Staff `catalog.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/menus/{id} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "page_3",
    "slug": "returns",
    "title": "Returns Policy",
    "status": "published"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---
