# Orange Wine — API Part 00 — Overview, Conventions and Catalogs

Version 1.0 — 28 Sep 2026 · 276 endpoints in 17 groups · Base URL `/api/v1`

## 1. How to read this document

This is the backend handover reference for every Orange Wine API endpoint. Each endpoint has: a metadata table (auth, permission, CSRF, idempotency, status), path/query parameters, a full request example followed by a key–value table for its fields, one or more response examples each followed by their key–value table, the error codes it can return, and notes.

Source of truth: `cursor-final-docs/05-api-contract.md` (routes, statuses, permissions) and the decision register D-00…D-41. The authentication group follows the Part 1 Authentication detailed spec. Field names beyond the examples in 05 are the proposed canonical shapes and should be locked into the OpenAPI spec during Phase 1.

### Status legend

| Status | Meaning |
|---|---|
| CONFIRMED | Route, method and behavior are decided. Build it. |
| PROPOSED | Needed by a confirmed workflow but not named in the source contract. Build it this way unless the team changes it; listed in Appendix D. |
| CONFIGURATION GATE | Build the endpoint and its disabled envelope now. Real behavior switches on when the provider/legal/tax configuration is supplied. |

### Key–value tables

| Column | Meaning |
|---|---|
| Key | JSON key; dotted paths (`a.b`) for nested keys, `[]` for array items. |
| Type / allowed values | `string`, `integer`, `boolean`, Money, ISO 8601, enum values separated by `\|`, plus `required` / `optional` / `response only`. |
| Description | Meaning, rules and the decision reference where relevant. |

## 2. Global conventions

### 2.1 Base URL and format

- All endpoints are under `/api/v1` except inbound webhooks (`/webhooks/...`).
- JSON request and response bodies (UTF-8). File uploads use `multipart/form-data`.
- Keys are `snake_case`. Enum values are lowercase `snake_case`.
- Timestamps are ISO 8601 UTC (`2026-09-17T16:00:00Z`). Store-local dates use `YYYY-MM-DD`; times use `HH:MM` with the location's IANA timezone.
- IDs are opaque prefixed strings (`prod_101`, `ord_5001`). Never expose auto-increment integers. Order numbers (`OW-100245`) are human-facing.

### 2.2 Authentication and CSRF

- Sessions use HttpOnly, Secure, SameSite=Lax cookies: a short-lived JWT access cookie and an opaque rotating refresh cookie. Tokens never appear in JSON bodies.
- One login endpoint for customers and staff: `POST /auth/login`. Staff permissions come from `GET /auth/me`.
- Every non-GET browser request must send `X-XSRF-TOKEN` (value of the `XSRF-TOKEN` cookie from `GET /auth/csrf`). Webhooks are exempt.
- Guests access their orders with a guest-order proof cookie issued by `POST /orders/guest-lookup` (order number + email or phone).

### 2.3 Money

- Always an object: `{ "amount_minor": 6275, "currency": "USD" }`. `amount_minor` is an integer in cents. Never floats, never bare numbers.
- Percentages are basis points (`1500` = 15%).

### 2.4 Idempotency

- Money-moving and state-creating commands require an `Idempotency-Key` header (UUID v4). Never in the body.
- Same key + same body → the original response is replayed. Same key + different body → `409 IDEMPOTENCY_KEY_REUSED`.
- Retention window is configured later. Endpoints needing a key are listed in Appendix C.

### 2.5 State changes

- Status never changes through PATCH. Every transition is a named command (`/approve`, `/pick`, `/release`, ...).
- Invalid transitions return `409 INVALID_STATE_TRANSITION` with `details.current_status` and `details.allowed_actions`.

### 2.6 Pagination, sorting, filtering

- Query `page` (default 1), `per_page` (default 24, max 100), `sort` (`-created_at` for descending).
- Lists return `data.items[]` plus `data.pagination { page, per_page, total, last_page }`.

### 2.7 Rate limiting

- Rate-limited endpoints return `429 RATE_LIMITED` with a `Retry-After` header. The numbers are configured later.

### 2.8 Inventory rule

- `available = on_hand − reserved − allocated`. Pre-arrival supply is never on hand or available. Every change writes an inventory movement.

### 2.9 Standard headers

| Header | Direction | Rule |
|---|---|---|
| Content-Type | Request | `application/json` (or `multipart/form-data` for uploads). |
| Accept | Request | `application/json`. |
| X-XSRF-TOKEN | Request | Required on every non-GET browser request. |
| Idempotency-Key | Request | UUID v4 on commands marked Idempotency = Required. |
| X-Request-Id | Request (optional) | Client-generated; echoed as `request_id`. |
| X-Location-Id | Request (POS/dashboard, PROPOSED) | Active store for staff who work at several locations; must be one of their assignments. |
| Retry-After | Response | Seconds; with `RATE_LIMITED` / `PROVIDER_UNAVAILABLE`. |

### 2.10 Success envelope

```json
{
  "data": {
    "id": "prod_101",
    "name": "Example Cabernet 2023 750ml"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

| Key | Type / allowed values | Description |
|---|---|---|
| data | object \| array | The payload. Lists put items in `data.items`. |
| request_id | string | Unique per request; log it for support. |
| correlation_id | string | Traces a business flow across services, jobs and webhooks. |

### 2.11 Error envelope

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The given data was invalid.",
    "details": {
      "fields": {
        "email": [
          "The email field is required."
        ]
      }
    },
    "retryable": false
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

| Key | Type / allowed values | Description |
|---|---|---|
| error.code | enum | Stable machine code (Appendix A). Clients branch on this, never on message. |
| error.message | string | Human-readable, safe to show. |
| error.details | object | Code-specific. `fields` for validation; `current_status`/`allowed_actions` for transitions. |
| error.retryable | boolean | `true` only for `RATE_LIMITED`, `PROVIDER_UNAVAILABLE`, `PAYMENT_PENDING`. |

### 2.12 Disabled (configuration gate) envelope

```json
{
  "data": {
    "enabled": false,
    "status": "pending_configuration",
    "reason_code": "PROVIDER_NOT_CONFIGURED",
    "message": "Local delivery is not available yet."
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

| Key | Type / allowed values | Description |
|---|---|---|
| data.enabled | boolean | `false` while gated. |
| data.status | enum | `pending_configuration`. |
| data.reason_code | string | e.g. `PROVIDER_NOT_CONFIGURED`, `LEGAL_APPROVAL_PENDING`, `TAX_NOT_CONFIGURED`. |
| data.message | string | Safe to display. |

### 2.13 Webhook acknowledgement

```json
{
  "received": true,
  "duplicate": false
}
```

Webhooks are the only responses without the envelope.

## 3. Endpoint index

| Part | Covers | Endpoints |
|---|---|---|
| Part 01 | System and Authentication | 16 |
| Part 02 | Customer Account | 11 |
| Part 03 | Storefront Catalog and Locations | 37 |
| Part 04 | Cart and Checkout | 16 |
| Part 05 | Orders, Cancellations, Returns and Refunds | 22 |
| Part 06 | Shipping, Delivery and Fulfillment | 19 |
| Part 07 | Inventory | 18 |
| Part 08 | Native POS | 25 |
| Part 09 | Gift Cards and Store Credit | 8 |
| Part 10 | Catalog Administration | 56 |
| Part 11 | Pricing and Promotions | 19 |
| Part 12 | Staff, Roles, Approvals, Reports and Notifications | 21 |
| Part 13 | Pre-arrival Operations and Webhooks | 8 |
|  | Total | 276 |

| Status | Count |
|---|---|
| CONFIRMED | 244 |
| CONFIGURATION GATE | 8 |
| CONFIRMED (provider CONFIGURATION GATE) | 1 |
| CONFIRMED rule · PROPOSED shape | 1 |
| PROPOSED | 19 |
| CONFIGURATION GATE (deny by default) | 1 |
| CONFIGURATION GATE (D-36) | 2 |

## Appendix A. Error catalog

| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
| `AUTHENTICATION_REQUIRED` | 401 | No | No valid session, or the access token has expired and refresh failed. |
| `FORBIDDEN` | 403 | No | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `VALIDATION_ERROR` | 422 | No | Request failed validation. `details.fields` maps field → messages. |
| `RESOURCE_NOT_FOUND` | 404 | No | Resource doesn't exist or isn't visible to the caller (never reveals existence). |
| `RATE_LIMITED` | 429 | Yes | Too many requests. `Retry-After` header set. Limits are configured later. |
| `IDEMPOTENCY_KEY_REUSED` | 409 | No | Same `Idempotency-Key` sent with a different request body. |
| `INVENTORY_CONFLICT` | 409 | No | Requested quantity no longer available at the fulfilling location. |
| `INVENTORY_ALLOCATION_CONFLICT` | 409 | No | Allocation or release could not be completed consistently. |
| `CHECKOUT_EXPIRED` | 409 | No | Checkout session expired; start a new session. |
| `QUOTE_STALE` | 409 | No | Cart, prices, promotions or fulfillment changed since the last quote; re-quote. |
| `PAYMENT_PENDING` | 202 | Yes | Provider hasn't confirmed the payment yet; poll the payment status. |
| `PAYMENT_FAILED` | 402 | No | Provider declined or failed the payment. |
| `PAYMENT_RECONCILIATION_REQUIRED` | 409 | No | Payment state is ambiguous; staff reconciliation is required. |
| `PAYMENT_ALREADY_CAPTURED` | 409 | No | The payment/order was already captured; returns the existing result where possible. |
| `REGISTER_SESSION_REQUIRED` | 409 | No | POS action requires an open register session. |
| `REGISTER_ALREADY_OPEN` | 409 | No | The register already has an open session. |
| `AGE_VERIFICATION_REQUIRED` | 422 | No | Age verification must be recorded before this step. |
| `FULFILLMENT_NOT_ELIGIBLE` | 422 | No | Selected fulfillment mode isn't allowed for this cart/location/time. |
| `SHIPPING_DESTINATION_NOT_ELIGIBLE` | 422 | No | Destination state/address isn't eligible for shipping. |
| `ADULT_SIGNATURE_REQUIRED` | 422 | No | Shipment must use adult-signature service. |
| `PRE_ARRIVAL_TERMS_MISSING` | 422 | No | Pre-arrival terms not accepted for this checkout. |
| `CANCELLATION_NOT_ELIGIBLE` | 422 | No | Normal order outside the 24-hour window or already picked. |
| `PRE_ARRIVAL_CANCELLATION_NOT_ELIGIBLE` | 422 | No | Pre-arrival order doesn't meet an approved cancellation reason. |
| `CANCELLATION_REQUEST_ALREADY_OPEN` | 409 | No | A cancellation request is already open for this order. |
| `RETURN_WINDOW_EXPIRED` | 422 | No | Outside the 30-day return window. |
| `RETURN_NOT_ELIGIBLE` | 422 | No | Item/reason combination isn't returnable. |
| `RETURN_REVIEW_REQUIRED` | 409 | No | Return must be reviewed/inspected before this action. |
| `INVALID_RETURN_STATE` | 409 | No | Return is not in a state that allows this action. |
| `INVALID_STATE_TRANSITION` | 409 | No | Named command not allowed from the resource's current state. |
| `REFUND_APPROVAL_REQUIRED` | 403 | No | Refund requires a valid manager approval. |
| `REFUND_ALREADY_ISSUED` | 409 | No | Refund for this scope was already issued. |
| `PROMOTION_RULE_CONFLICT` | 409 | No | Promotion conflicts with another active non-stackable rule. |
| `PROVIDER_UNAVAILABLE` | 503 | Yes | External provider unreachable or not configured; safe to retry later. |
| `WEBHOOK_REPLAY` | 200 | No | Internal: duplicate provider event; acknowledged with `duplicate: true`. |

## Appendix B. Permission catalog

| Permission | Grants | Endpoints |
|---|---|---|
| `catalog.manage` | Products, taxonomy, media, content, price books | 62 |
| `gift_cards.manage` | Gift cards and store credit | 3 |
| `inventory.adjust` | Adjustments and counts (manager approval above threshold) | 4 |
| `inventory.receive` | Receive purchase orders, transfers and pre-arrival releases | 3 |
| `inventory.transfer` | Create/ship transfers | 4 |
| `inventory.view` | Read balances, movements, reservations, allocations | 5 |
| `locations.manage` | Locations, hours, holidays, closures | 12 |
| `notifications.manage` | Resend notifications (PROPOSED) | 1 |
| `orders.fulfill` | Dashboard orders, fulfillment steps, cancellation approvals, return review/receive/inspect | 21 |
| `pos.refund` | Issue refunds (always with manager approval) | 3 |
| `pos.sell` | Create tickets, take payment (overrides/voids also need manager approval) | 13 |
| `pre_arrivals.manage` | Pre-arrival records (PROPOSED) | 6 |
| `promotions.manage` | Promotions, coupons, product program flags, shipping-program settings | 13 |
| `register.close` | Close register sessions | 1 |
| `register.open` | Open register sessions | 1 |
| `reports.view` | Reports | 1 |
| `shipping.manage` | Labels, void labels, Rapid Ship SLA | 3 |
| `staff.manage` | Staff, roles, assignments, staff sessions | 14 |

## Appendix C. Endpoints requiring Idempotency-Key

| Method | Path | Title |
|---|---|---|
| POST | `/cart/merge` | Merge guest cart after sign-in |
| POST | `/checkout/sessions` | Start checkout |
| POST | `/checkout/sessions/{checkoutSessionId}/quote` | Re-quote |
| POST | `/checkout/sessions/{checkoutSessionId}/fulfillment` | Set fulfillment and address |
| POST | `/checkout/sessions/{checkoutSessionId}/pre-arrival-terms` | Accept Pre-Arrival Terms |
| POST | `/checkout/sessions/{checkoutSessionId}/payment-intent` | Create payment |
| POST | `/checkout/sessions/{checkoutSessionId}/place-order` | Place order |
| POST | `/orders/{orderId}/cancellation-requests` | Request cancellation |
| POST | `/dashboard/orders/{orderId}/cancellation-requests/{requestId}/approve` | Approve cancellation |
| POST | `/dashboard/orders/{orderId}/cancellation-requests/{requestId}/reject` | Reject cancellation |
| POST | `/orders/{orderId}/returns` | Request return |
| POST | `/dashboard/returns/{returnId}/approve` | Approve return |
| POST | `/dashboard/returns/{returnId}/reject` | Reject return |
| POST | `/dashboard/returns/{returnId}/receive` | Mark received |
| POST | `/dashboard/returns/{returnId}/inspect` | Record inspection |
| POST | `/dashboard/orders/{orderId}/refunds` | Issue refund |
| POST | `/dashboard/fulfillments/{fulfillmentId}/pick` | Pick |
| POST | `/dashboard/fulfillments/{fulfillmentId}/pack` | Pack (PROPOSED step) |
| POST | `/dashboard/fulfillments/{fulfillmentId}/ready` | Ready for pickup |
| POST | `/dashboard/fulfillments/{fulfillmentId}/ship` | Ship |
| POST | `/dashboard/fulfillments/{fulfillmentId}/handoff` | Hand over to customer |
| POST | `/dashboard/fulfillments/{fulfillmentId}/exception` | Record exception |
| POST | `/dashboard/shipments/{shipmentId}/label` | Buy label |
| POST | `/dashboard/shipments/{shipmentId}/void-label` | Void label |
| POST | `/dashboard/inventory/exceptions/{exceptionId}/resolve` | Resolve exception |
| POST | `/dashboard/inventory/adjustments` | Adjust stock |
| POST | `/dashboard/inventory/counts/{countId}/submit` | Submit count |
| POST | `/dashboard/purchase-orders/{purchaseOrderId}/receive` | Receive PO |
| POST | `/dashboard/transfers/{transferId}/approve` | Approve transfer |
| POST | `/dashboard/transfers/{transferId}/ship` | Ship transfer |
| POST | `/dashboard/transfers/{transferId}/receive` | Receive transfer |
| POST | `/pos/register-sessions/open` | Open register |
| POST | `/pos/register-sessions/{registerSessionId}/close` | Close register |
| POST | `/pos/register-sessions/{registerSessionId}/reconcile` | Reconcile register |
| POST | `/pos/tickets/{ticketId}/price-override` | Price override |
| POST | `/pos/tickets/{ticketId}/void` | Void ticket |
| POST | `/pos/tickets/{ticketId}/payment` | Take payment and complete sale |
| POST | `/pos/sales/{saleId}/returns` | In-store return |
| POST | `/dashboard/gift-cards` | Issue gift card |
| POST | `/dashboard/gift-cards/{giftCardId}/activate` | Activate |
| POST | `/dashboard/gift-cards/{giftCardId}/adjust` | Adjust balance |
| POST | `/dashboard/gift-cards/{giftCardId}/deactivate` | Deactivate |
| POST | `/dashboard/store-credit/{customerId}/adjust` | Adjust store credit |
| POST | `/dashboard/staff` | Create staff |
| POST | `/dashboard/staff/{staffId}/disable` | Disable staff |
| POST | `/dashboard/manager-approvals` | Issue manager approval |
| POST | `/dashboard/pre-arrivals` | Create pre-arrival record |
| POST | `/dashboard/pre-arrivals/{preArrivalId}/delay` | Record supplier delay |
| POST | `/dashboard/pre-arrivals/{preArrivalId}/release` | Release (stock arrived) |

## Appendix D. PROPOSED endpoints and configuration gates

| Status | Method | Path | Group |
|---|---|---|---|
| CONFIGURATION GATE | POST | `/auth/staff/mfa/challenge` | Authentication |
| CONFIGURATION GATE | POST | `/addresses/validate` | Locations, Hours and Addresses |
| CONFIRMED (provider CONFIGURATION GATE) | POST | `/checkout/sessions/{checkoutSessionId}/payment-intent` | Checkout and Payments |
| CONFIRMED rule · PROPOSED shape | POST | `/orders/guest-lookup` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | POST | `/orders/{orderId}/curbside-arrival` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/cancellation-requests` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | POST | `/orders/{orderId}/return-attachments` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/returns` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/returns/{returnId}` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/orders/{orderId}/refunds` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/orders` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| PROPOSED | GET | `/dashboard/orders/{orderId}` | Orders, Guest Lookup, Cancellations, Returns and Refunds |
| CONFIGURATION GATE | POST | `/shipping/quote` | Shipping, Fulfillment and Rapid Ship |
| CONFIGURATION GATE (deny by default) | GET | `/shipping/eligibility` | Shipping, Fulfillment and Rapid Ship |
| CONFIGURATION GATE (D-36) | GET | `/delivery/zones` | Shipping, Fulfillment and Rapid Ship |
| CONFIGURATION GATE (D-36) | POST | `/delivery/estimate` | Shipping, Fulfillment and Rapid Ship |
| CONFIGURATION GATE | POST | `/dashboard/shipments/{shipmentId}/label` | Shipping, Fulfillment and Rapid Ship |
| CONFIGURATION GATE | POST | `/dashboard/shipments/{shipmentId}/void-label` | Shipping, Fulfillment and Rapid Ship |
| PROPOSED | GET | `/dashboard/rapid-ship/sla` | Shipping, Fulfillment and Rapid Ship |
| PROPOSED | GET | `/dashboard/inventory/exceptions` | Inventory |
| PROPOSED | POST | `/dashboard/inventory/exceptions/{exceptionId}/resolve` | Inventory |
| PROPOSED | POST | `/dashboard/transfers/{transferId}/approve` | Inventory |
| PROPOSED | GET | `/dashboard/product-groups` | Catalog Administration |
| PROPOSED | POST | `/dashboard/product-groups` | Catalog Administration |
| PROPOSED | PATCH | `/dashboard/product-groups/{groupId}` | Catalog Administration |
| PROPOSED | DELETE | `/dashboard/product-groups/{groupId}` | Catalog Administration |
| PROPOSED | GET | `/dashboard/settings/shipping-programs` | Pricing and Promotions |
| PROPOSED | PATCH | `/dashboard/settings/shipping-programs` | Pricing and Promotions |
| CONFIGURATION GATE | POST | `/dashboard/staff/{staffId}/mfa/enroll` | Staff, Roles, Approvals, Reports and Notifications |
| PROPOSED | POST | `/dashboard/pre-arrivals` | Pre-arrival Operations |
| CONFIGURATION GATE | POST | `/webhooks/payments/{provider}` | Webhooks (inbound) |
| CONFIGURATION GATE | POST | `/webhooks/shipping/{provider}` | Webhooks (inbound) |

## Appendix E. Open items affecting the API

| Topic | Status |
|---|---|
| Providers | Payment, tax, shipping (Shippo), address validation, email/SMS providers — configuration gates. Endpoints return the disabled envelope until configured. |
| Rate limits | Numbers configured later. Endpoints marked rate-limited must return `RATE_LIMITED` + `Retry-After`. |
| Pagination | `per_page` above 100: reject (current) vs clamp — confirm. |
| 12 Ship Free | Service-level restriction and surcharge waiver — default none / not waived. |
| Rapid Ship | SLA measured from order vs dispatch; SLA refund amount rule. |
| Tax classes | Final `tax_class` value set depends on the tax provider. |
| Manager PIN | Short PIN vs full re-authentication for shared terminals. |
| Staff MFA | Enrollment flow is a configuration gate. |
| PROPOSED endpoints | Every endpoint marked PROPOSED fills a gap required by a confirmed workflow; confirm when writing the OpenAPI spec. |
