# Orange Wine — API Part 11: Pricing and Promotions

Version 1.0 — 28 Sep 2026 · 19 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 |
|---|---|---|
| `PROMOTION_RULE_CONFLICT` | 409 | Promotion conflicts with another active non-stackable rule. |
| `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 | 6 |
| `promotions.manage` | Promotions, coupons, product program flags, shipping-program settings | 13 |

### PROPOSED and gated endpoints in this part

| Status | Method | Path |
|---|---|---|
| PROPOSED | GET | `/dashboard/settings/shipping-programs` |
| PROPOSED | PATCH | `/dashboard/settings/shipping-programs` |

## 2. Pricing and Promotions

Price books and promotions (D-17). Promotions are versioned; orders snapshot the version. 12 Ship Free and Rapid Ship are NOT promotions — they are product flags plus shipping-program settings.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/dashboard/price-books` | Price books | CONFIRMED |
| 2.2 | POST | `/dashboard/price-books` | Create price book | CONFIRMED |
| 2.3 | GET | `/dashboard/price-books/{priceBookId}` | Price book detail | CONFIRMED |
| 2.4 | PATCH | `/dashboard/price-books/{priceBookId}` | Update price book | CONFIRMED |
| 2.5 | GET | `/dashboard/price-books/{priceBookId}/entries` | Price entries | CONFIRMED |
| 2.6 | POST | `/dashboard/price-books/{priceBookId}/entries` | Upsert price entries | CONFIRMED |
| 2.7 | GET | `/dashboard/promotions` | Promotions | CONFIRMED |
| 2.8 | POST | `/dashboard/promotions` | Create promotion | CONFIRMED |
| 2.9 | GET | `/dashboard/promotions/{promotionId}` | Promotion detail | CONFIRMED |
| 2.10 | PATCH | `/dashboard/promotions/{promotionId}` | Update promotion (new version) | CONFIRMED |
| 2.11 | GET | `/dashboard/promotions/{promotionId}/exclusions` | Exclusions | CONFIRMED |
| 2.12 | PUT | `/dashboard/promotions/{promotionId}/exclusions` | Replace exclusions | CONFIRMED |
| 2.13 | GET | `/dashboard/coupons` | Coupons | CONFIRMED |
| 2.14 | POST | `/dashboard/coupons` | Create coupon | CONFIRMED |
| 2.15 | GET | `/dashboard/coupons/{couponId}` | Coupon detail | CONFIRMED |
| 2.16 | PATCH | `/dashboard/coupons/{couponId}` | Update coupon | CONFIRMED |
| 2.17 | GET | `/dashboard/coupons/{couponId}/redemptions` | Coupon redemptions | CONFIRMED |
| 2.18 | GET | `/dashboard/settings/shipping-programs` | Shipping program settings | PROPOSED |
| 2.19 | PATCH | `/dashboard/settings/shipping-programs` | Update shipping program settings | PROPOSED |

### 2.1 `GET /dashboard/price-books`

**Price books**

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

**Request example**

```http
GET /api/v1/dashboard/price-books 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": "pb_base",
        "name": "Base",
        "scope": "organization",
        "location_id": null,
        "region": null,
        "status": "active",
        "effective_from": "2026-09-01T04:00:00Z",
        "effective_to": null
      }
    ],
    "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 |
|---|---|---|
| scope | enum | `organization` \| `location` \| `region` (no legal-entity scope, D-19). |

---

### 2.2 `POST /dashboard/price-books`

**Create price book**

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

**Request example**

```http
POST /api/v1/dashboard/price-books 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": "Albany overrides",
  "scope": "location",
  "location_id": "loc_albany",
  "effective_from": "2026-10-01T04:00:00Z"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| scope | enum · required |  |
| location_id / region | required by scope |  |
| effective_from / effective_to | ISO 8601 · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "pb_alb",
    "status": "draft"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.3 `GET /dashboard/price-books/{priceBookId}`

**Price book detail**

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

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pb_alb",
    "name": "Albany overrides",
    "scope": "location",
    "entry_count": 120
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.4 `PATCH /dashboard/price-books/{priceBookId}`

**Update price book**

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

**Path parameters**

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

**Request example**

```http
PATCH /api/v1/dashboard/price-books/{priceBookId} 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>

{
  "effective_to": "2026-12-31T04:59:59Z"
}
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "pb_alb"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.5 `GET /dashboard/price-books/{priceBookId}/entries`

**Price entries**

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

**Path parameters**

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

**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/price-books/{priceBookId}/entries?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": [
      {
        "product_id": "prod_101",
        "price": {
          "amount_minor": 5999,
          "currency": "USD"
        },
        "cost": {
          "amount_minor": 3500,
          "currency": "USD"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.6 `POST /dashboard/price-books/{priceBookId}/entries`

**Upsert price entries**

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

**Path parameters**

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

**Request example**

```http
POST /api/v1/dashboard/price-books/{priceBookId}/entries 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>

{
  "entries": [
    {
      "product_id": "prod_101",
      "price": {
        "amount_minor": 5999,
        "currency": "USD"
      }
    }
  ]
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| entries[].product_id | string · required |  |
| entries[].price | Money object `{ amount_minor: integer, currency: "USD" }` · required |  |
| entries[].cost | Money · optional |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "upserted": 1
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.7 `GET /dashboard/promotions`

**Promotions**

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

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum · optional | `draft` \| `scheduled` \| `active` \| `expired` \| `disabled`. |
| 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/promotions?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": "promo_5",
        "name": "Welcome 10%",
        "version": 3,
        "status": "active",
        "type": "percentage",
        "value_bps": 1000,
        "scope": {
          "level": "order",
          "product_ids": [],
          "category_ids": []
        },
        "coupon_required": true,
        "stackable": false,
        "priority": 10,
        "starts_at": "2026-09-01T04:00:00Z",
        "ends_at": null,
        "restrictions": {
          "location_ids": [],
          "fulfillment_modes": [],
          "states": [],
          "customer_segment": "new_customers"
        },
        "limits": {
          "total_redemptions": 1000,
          "per_customer": 1
        }
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.8 `POST /dashboard/promotions`

**Create promotion**

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

**Request example**

```http
POST /api/v1/dashboard/promotions 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": "Welcome 10%",
  "type": "percentage",
  "value_bps": 1000,
  "scope": {
    "level": "order",
    "product_ids": [],
    "category_ids": []
  },
  "coupon_required": true,
  "stackable": false,
  "priority": 10,
  "starts_at": "2026-09-01T04:00:00Z",
  "ends_at": null,
  "restrictions": {
    "location_ids": [],
    "fulfillment_modes": [],
    "states": [],
    "customer_segment": "new_customers"
  },
  "limits": {
    "total_redemptions": 1000,
    "per_customer": 1
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| type | enum · required | `percentage` \| `fixed_amount` \| `quantity_break` \| `case_pack_offer` \| `bogo` (bogo PROPOSED). |
| value_bps / value_amount | integer / Money | By type. |
| scope.level | enum · required | `order` \| `line`. |
| scope.product_ids / category_ids | string[] · optional |  |
| coupon_required | boolean |  |
| stackable | boolean |  |
| priority | integer | Higher applies first. |
| starts_at / ends_at | ISO 8601 |  |
| restrictions.* | object | Locations, fulfillment modes, states, customer segment. |
| limits.total_redemptions / per_customer | integer \| null |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "promo_5",
    "name": "Welcome 10%",
    "version": 1,
    "status": "draft",
    "type": "percentage",
    "value_bps": 1000,
    "scope": {
      "level": "order",
      "product_ids": [],
      "category_ids": []
    },
    "coupon_required": true,
    "stackable": false,
    "priority": 10,
    "starts_at": "2026-09-01T04:00:00Z",
    "ends_at": null,
    "restrictions": {
      "location_ids": [],
      "fulfillment_modes": [],
      "states": [],
      "customer_segment": "new_customers"
    },
    "limits": {
      "total_redemptions": 1000,
      "per_customer": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `PROMOTION_RULE_CONFLICT` | 409 | Overlapping non-stackable active rule |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

---

### 2.9 `GET /dashboard/promotions/{promotionId}`

**Promotion detail**

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

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "promo_5",
    "name": "Welcome 10%",
    "version": 3,
    "status": "active",
    "type": "percentage",
    "value_bps": 1000,
    "scope": {
      "level": "order",
      "product_ids": [],
      "category_ids": []
    },
    "coupon_required": true,
    "stackable": false,
    "priority": 10,
    "starts_at": "2026-09-01T04:00:00Z",
    "ends_at": null,
    "restrictions": {
      "location_ids": [],
      "fulfillment_modes": [],
      "states": [],
      "customer_segment": "new_customers"
    },
    "limits": {
      "total_redemptions": 1000,
      "per_customer": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.10 `PATCH /dashboard/promotions/{promotionId}`

**Update promotion (new version)**

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

**Path parameters**

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

**Request example**

```http
PATCH /api/v1/dashboard/promotions/{promotionId} 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>

{
  "value_bps": 1500
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any create field) | optional | Creates a new version; past orders keep theirs. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "promo_5",
    "name": "Welcome 10%",
    "version": 4,
    "status": "active",
    "type": "percentage",
    "value_bps": 1500,
    "scope": {
      "level": "order",
      "product_ids": [],
      "category_ids": []
    },
    "coupon_required": true,
    "stackable": false,
    "priority": 10,
    "starts_at": "2026-09-01T04:00:00Z",
    "ends_at": null,
    "restrictions": {
      "location_ids": [],
      "fulfillment_modes": [],
      "states": [],
      "customer_segment": "new_customers"
    },
    "limits": {
      "total_redemptions": 1000,
      "per_customer": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `PROMOTION_RULE_CONFLICT` | 409 | Promotion conflicts with another active non-stackable rule. |

---

### 2.11 `GET /dashboard/promotions/{promotionId}/exclusions`

**Exclusions**

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

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "product_ids": [
      "prod_900"
    ],
    "category_ids": [],
    "brand_ids": []
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.12 `PUT /dashboard/promotions/{promotionId}/exclusions`

**Replace exclusions**

| Property | Value |
|---|---|
| Endpoint | `PUT /api/v1/dashboard/promotions/{promotionId}/exclusions` |
| Auth | Staff `promotions.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
PUT /api/v1/dashboard/promotions/{promotionId}/exclusions 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_900"
  ],
  "category_ids": [],
  "brand_ids": []
}
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "product_ids": [
      "prod_900"
    ],
    "category_ids": [],
    "brand_ids": []
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.13 `GET /dashboard/coupons`

**Coupons**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/coupons` |
| Auth | Staff `promotions.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/coupons?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": "cpn_1",
        "code": "WELCOME10",
        "promotion_id": "promo_5",
        "status": "active",
        "redemptions": 88
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.14 `POST /dashboard/coupons`

**Create coupon**

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

**Request example**

```http
POST /api/v1/dashboard/coupons 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>

{
  "code": "WELCOME10",
  "promotion_id": "promo_5"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| code | string · required | Unique, case-insensitive. |
| promotion_id | string · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "cpn_1",
    "code": "WELCOME10",
    "status": "active"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.15 `GET /dashboard/coupons/{couponId}`

**Coupon detail**

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

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "cpn_1",
    "code": "WELCOME10",
    "promotion_id": "promo_5",
    "status": "active"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.16 `PATCH /dashboard/coupons/{couponId}`

**Update coupon**

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

**Path parameters**

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

**Request example**

```http
PATCH /api/v1/dashboard/coupons/{couponId} 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>

{
  "status": "disabled"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum · optional | `active` \| `disabled`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "cpn_1",
    "status": "disabled"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.17 `GET /dashboard/coupons/{couponId}/redemptions`

**Coupon redemptions**

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

**Path parameters**

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

**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/coupons/{couponId}/redemptions?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": [
      {
        "order_id": "ord_5001",
        "customer_id": "usr_101",
        "discount": {
          "amount_minor": 1000,
          "currency": "USD"
        },
        "redeemed_at": "2026-09-17T16:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.18 `GET /dashboard/settings/shipping-programs`

**Shipping program settings**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/settings/shipping-programs` |
| Auth | Staff `promotions.manage` |
| Status | PROPOSED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ship_free_12": {
      "enabled": true,
      "service_level_restriction": null,
      "waive_surcharges": false
    },
    "rapid_ship": {
      "enabled": true,
      "default_cutoff_local_time": "12:00",
      "delivery_target_days": 2,
      "carrier": "ups",
      "service_level": "2day",
      "sla_measured_from": "order",
      "sla_refund_rule": {
        "type": "staff_entered",
        "amount": null
      }
    },
    "cancellation": {
      "window_hours": 24
    },
    "fees": {
      "cancellation_fee": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "restocking_fee_bps": 1500,
      "non_refundable_payment_fee_enabled": false,
      "failed_delivery_charges_enabled": false
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| ship_free_12.service_level_restriction | string \| null | Open decision; default none. |
| ship_free_12.waive_surcharges | boolean | Open decision; default false. |
| rapid_ship.sla_measured_from | enum | `order` \| `dispatch` — open decision. |
| rapid_ship.sla_refund_rule.type | enum | `staff_entered` \| `full_shipping` \| `fixed_amount` — value open. |
| fees.* | mixed | Fee engine configuration (D-24). |

---

### 2.19 `PATCH /dashboard/settings/shipping-programs`

**Update shipping program settings**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/settings/shipping-programs` |
| Auth | Staff `promotions.manage` |
| Status | PROPOSED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
PATCH /api/v1/dashboard/settings/shipping-programs 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>

{
  "rapid_ship": {
    "default_cutoff_local_time": "12:00"
  },
  "cancellation": {
    "window_hours": 24
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any field above) | optional | Changes are audited and apply to new orders only. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "updated": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---
