# Orange Wine — API Part 08: Native POS

Version 1.0 — 28 Sep 2026 · 25 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 |
|---|---|---|
| `AGE_VERIFICATION_REQUIRED` | 422 | Age verification must be recorded before this step. |
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |
| `INVENTORY_CONFLICT` | 409 | Requested quantity no longer available at the fulfilling location. |
| `PAYMENT_ALREADY_CAPTURED` | 409 | The payment/order was already captured; returns the existing result where possible. |
| `PAYMENT_FAILED` | 402 | Provider declined or failed the payment. |
| `PAYMENT_RECONCILIATION_REQUIRED` | 409 | Payment state is ambiguous; staff reconciliation is required. |
| `REFUND_ALREADY_ISSUED` | 409 | Refund for this scope was already issued. |
| `REFUND_APPROVAL_REQUIRED` | 403 | Refund requires a valid manager approval. |
| `REGISTER_ALREADY_OPEN` | 409 | The register already has an open session. |
| `REGISTER_SESSION_REQUIRED` | 409 | POS action requires an open register session. |
| `RETURN_WINDOW_EXPIRED` | 422 | Outside the 30-day return window. |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

### Permissions used in this part

| Permission | Grants | Endpoints |
|---|---|---|
| `pos.refund` | Issue refunds (always with manager approval) | 1 |
| `pos.sell` | Create tickets, take payment (overrides/voids also need manager approval) | 13 |
| `register.close` | Close register sessions | 1 |
| `register.open` | Open register sessions | 1 |

### Endpoints requiring Idempotency-Key

| Method | Path | Title |
|---|---|---|
| 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 |

## 2. Native POS

Staff use the same cookie session as the dashboard (no Bearer token). No sale without an open, authorized register. Staff identity always comes from the session — never from the request body. Offline completion is not available in V1.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/pos/registers` | Registers at my locations | CONFIRMED |
| 2.2 | GET | `/pos/register-sessions/current` | My current register session | CONFIRMED |
| 2.3 | POST | `/pos/register-sessions/open` | Open register | CONFIRMED |
| 2.4 | GET | `/pos/register-sessions/{registerSessionId}` | Register session detail | CONFIRMED |
| 2.5 | POST | `/pos/register-sessions/{registerSessionId}/suspend` | Suspend (break) | CONFIRMED |
| 2.6 | POST | `/pos/register-sessions/{registerSessionId}/resume` | Resume | CONFIRMED |
| 2.7 | POST | `/pos/register-sessions/{registerSessionId}/close` | Close register | CONFIRMED |
| 2.8 | POST | `/pos/register-sessions/{registerSessionId}/reconcile` | Reconcile register | CONFIRMED |
| 2.9 | GET | `/pos/products/search` | POS product lookup | CONFIRMED |
| 2.10 | GET | `/pos/customers/search` | Find customer | CONFIRMED |
| 2.11 | POST | `/pos/customers` | Create customer at POS | CONFIRMED |
| 2.12 | POST | `/pos/tickets` | New ticket | CONFIRMED |
| 2.13 | GET | `/pos/tickets/{ticketId}` | Get ticket | CONFIRMED |
| 2.14 | PATCH | `/pos/tickets/{ticketId}` | Update ticket (allow-list) | CONFIRMED |
| 2.15 | POST | `/pos/tickets/{ticketId}/lines` | Add line | CONFIRMED |
| 2.16 | PATCH | `/pos/tickets/{ticketId}/lines/{lineId}` | Change line quantity | CONFIRMED |
| 2.17 | DELETE | `/pos/tickets/{ticketId}/lines/{lineId}` | Remove line | CONFIRMED |
| 2.18 | POST | `/pos/tickets/{ticketId}/price-override` | Price override | CONFIRMED |
| 2.19 | POST | `/pos/tickets/{ticketId}/hold` | Hold ticket | CONFIRMED |
| 2.20 | POST | `/pos/tickets/{ticketId}/resume` | Resume ticket | CONFIRMED |
| 2.21 | POST | `/pos/tickets/{ticketId}/void` | Void ticket | CONFIRMED |
| 2.22 | POST | `/pos/tickets/{ticketId}/age-check` | Record age check | CONFIRMED |
| 2.23 | POST | `/pos/tickets/{ticketId}/payment` | Take payment and complete sale | CONFIRMED |
| 2.24 | GET | `/pos/sales/{saleId}/receipt` | Receipt | CONFIRMED |
| 2.25 | POST | `/pos/sales/{saleId}/returns` | In-store return | CONFIRMED |

### 2.1 `GET /pos/registers`

**Registers at my locations**

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

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| location_id | string · optional |  |

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "register_id": "reg_01",
        "name": "Front 1",
        "location_id": "loc_albany",
        "terminal_id": "term_01",
        "current_session_id": null
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.2 `GET /pos/register-sessions/current`

**My current register session**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/pos/register-sessions/current` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/pos/register-sessions/current HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "open",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `suspended` \| `close_pending` \| `closed` \| `reconciled`. |
| opening_float | Money object `{ amount_minor: integer, currency: "USD" }` |  |

**Response — none open (200)**

```http
HTTP/1.1 200 OK

{
  "data": null,
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.3 `POST /pos/register-sessions/open`

**Open register**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/register-sessions/open` |
| Auth | Staff `register.open` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Request example**

```http
POST /api/v1/pos/register-sessions/open 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "location_id": "loc_albany",
  "register_id": "reg_01",
  "opening_float": {
    "amount_minor": 50000,
    "currency": "USD"
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| location_id | string · required | Must be assigned to caller. |
| register_id | string · required |  |
| opening_float | Money object `{ amount_minor: integer, currency: "USD" }` · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "open",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `suspended` \| `close_pending` \| `closed` \| `reconciled`. |
| opening_float | Money object `{ amount_minor: integer, currency: "USD" }` |  |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `REGISTER_ALREADY_OPEN` | 409 | Register has an open session |
| `FORBIDDEN` | 403 | Location scope |

---

### 2.4 `GET /pos/register-sessions/{registerSessionId}`

**Register session detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/pos/register-sessions/{registerSessionId}` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
GET /api/v1/pos/register-sessions/{registerSessionId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "open",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    },
    "sales_count": 42,
    "expected_cash": {
      "amount_minor": 84250,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `suspended` \| `close_pending` \| `closed` \| `reconciled`. |
| opening_float | Money object `{ amount_minor: integer, currency: "USD" }` |  |

---

### 2.5 `POST /pos/register-sessions/{registerSessionId}/suspend`

**Suspend (break)**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/register-sessions/{registerSessionId}/suspend` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/register-sessions/{registerSessionId}/suspend 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": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "suspended",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |

---

### 2.6 `POST /pos/register-sessions/{registerSessionId}/resume`

**Resume**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/register-sessions/{registerSessionId}/resume` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/register-sessions/{registerSessionId}/resume 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": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "open",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Not suspended |

---

### 2.7 `POST /pos/register-sessions/{registerSessionId}/close`

**Close register**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/register-sessions/{registerSessionId}/close` |
| Auth | Staff `register.close` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/register-sessions/{registerSessionId}/close 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "counted_tenders": {
    "cash": {
      "amount_minor": 84000,
      "currency": "USD"
    },
    "card": {
      "amount_minor": 152300,
      "currency": "USD"
    }
  },
  "over_short_reason": "Short $2.50 — change error"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| counted_tenders.<tender> | Money object `{ amount_minor: integer, currency: "USD" }` · required | Tenders: `cash`, `card`, `gift_card`, `store_credit`. |
| over_short_reason | string · required when over/short ≠ 0 |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "closed",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    },
    "expected_cash": {
      "amount_minor": 84250,
      "currency": "USD"
    },
    "counted_cash": {
      "amount_minor": 84000,
      "currency": "USD"
    },
    "over_short": {
      "amount_minor": -250,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| over_short | Money object `{ amount_minor: integer, currency: "USD" }` | Negative = short. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Missing reason |
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |

---

### 2.8 `POST /pos/register-sessions/{registerSessionId}/reconcile`

**Reconcile register**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/register-sessions/{registerSessionId}/reconcile` |
| Auth | Staff (manager) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/register-sessions/{registerSessionId}/reconcile 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "manager_approval_id": "approval_1005",
  "notes": "Over/short accepted."
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| manager_approval_id | string · required when over/short ≠ 0 |  |
| notes | string · optional |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "register_session_id": "rs_1001",
    "register_id": "reg_01",
    "location_id": "loc_albany",
    "status": "reconciled",
    "opened_by": "staff_02",
    "opened_at": "2026-09-17T13:00:00Z",
    "opening_float": {
      "amount_minor": 50000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Not closed |

---

### 2.9 `GET /pos/products/search`

**POS product lookup**

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

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| barcode | string · optional | Scanner input (preferred). |
| q | string · optional | Name/SKU. |
| location_id | string · required |  |

**Request example**

```http
GET /api/v1/pos/products/search 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",
        "sku": "CAB-2023-750",
        "barcode": "012345678905",
        "name": "Example Cabernet 2023 750ml",
        "price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "available": 17,
        "age_restricted": true
      }
    ],
    "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 |
|---|---|---|
| available | integer | Live quantity at this store (staff only). |

---

### 2.10 `GET /pos/customers/search`

**Find customer**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/pos/customers/search` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| q | string · required | Name, email or phone (min 3 chars). |

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "customer_id": "usr_101",
        "name": "John Smith",
        "email_masked": "c***@example.com",
        "phone_masked": "***0100",
        "store_credit_balance": {
          "amount_minor": 2500,
          "currency": "USD"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.11 `POST /pos/customers`

**Create customer at POS**

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

**Request example**

```http
POST /api/v1/pos/customers 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>

{
  "first_name": "Asha",
  "last_name": "Patel",
  "email": "asha@example.com",
  "phone": "+15185550111"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| first_name / last_name | string · required |  |
| email | string · optional |  |
| phone | string · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "customer_id": "usr_102",
    "name": "Asha Patel"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

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

---

### 2.12 `POST /pos/tickets`

**New ticket**

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

**Request example**

```http
POST /api/v1/pos/tickets 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>

{
  "register_session_id": "rs_1001"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| register_session_id | string · required | Must be `open`. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [],
    "totals": {
      "subtotal": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 0,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `held` \| `payment_pending` \| `completed` \| `voided` \| `expired`. |
| lines[].override_price | Money \| null | Set via price-override with manager approval. |
| totals.* | Money object `{ amount_minor: integer, currency: "USD" }` | Calculated by the server. |
| age_check_required | boolean | Any age-restricted line. |
| age_check_result | enum \| null | `passed` \| `failed` \| `refused` \| `unavailable`. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `REGISTER_SESSION_REQUIRED` | 409 | POS action requires an open register session. |

---

### 2.13 `GET /pos/tickets/{ticketId}`

**Get ticket**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/pos/tickets/{ticketId}` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `held` \| `payment_pending` \| `completed` \| `voided` \| `expired`. |
| lines[].override_price | Money \| null | Set via price-override with manager approval. |
| totals.* | Money object `{ amount_minor: integer, currency: "USD" }` | Calculated by the server. |
| age_check_required | boolean | Any age-restricted line. |
| age_check_result | enum \| null | `passed` \| `failed` \| `refused` \| `unavailable`. |

---

### 2.14 `PATCH /pos/tickets/{ticketId}`

**Update ticket (allow-list)**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/pos/tickets/{ticketId}` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
PATCH /api/v1/pos/tickets/{ticketId} 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>

{
  "customer_id": "usr_101",
  "notes": "Gift wrap"
}
```

Status, price, totals, register or staff fields are rejected (VALIDATION_ERROR).

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| customer_id | string \| null · optional |  |
| notes | string · optional |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": "usr_101",
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.15 `POST /pos/tickets/{ticketId}/lines`

**Add line**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/lines` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/lines 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_id": "prod_101",
  "quantity": 1
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| product_id | string · required |  |
| quantity | integer · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `open` \| `held` \| `payment_pending` \| `completed` \| `voided` \| `expired`. |
| lines[].override_price | Money \| null | Set via price-override with manager approval. |
| totals.* | Money object `{ amount_minor: integer, currency: "USD" }` | Calculated by the server. |
| age_check_required | boolean | Any age-restricted line. |
| age_check_result | enum \| null | `passed` \| `failed` \| `refused` \| `unavailable`. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVENTORY_CONFLICT` | 409 | Requested quantity no longer available at the fulfilling location. |
| `INVALID_STATE_TRANSITION` | 409 | Ticket not open |

---

### 2.16 `PATCH /pos/tickets/{ticketId}/lines/{lineId}`

**Change line quantity**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/pos/tickets/{ticketId}/lines/{lineId}` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| ticketId | string |  |
| lineId | string |  |

**Request example**

```http
PATCH /api/v1/pos/tickets/{ticketId}/lines/{lineId} 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>

{
  "quantity": 2
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| quantity | integer · required · ≥ 1 |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVENTORY_CONFLICT` | 409 | Requested quantity no longer available at the fulfilling location. |

---

### 2.17 `DELETE /pos/tickets/{ticketId}/lines/{lineId}`

**Remove line**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/pos/tickets/{ticketId}/lines/{lineId}` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| ticketId | string |  |
| lineId | string |  |

**Request example**

```http
DELETE /api/v1/pos/tickets/{ticketId}/lines/{lineId} 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": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.18 `POST /pos/tickets/{ticketId}/price-override`

**Price override**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/price-override` |
| Auth | Staff + manager approval |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/price-override 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "line_id": "tl_1",
  "price": {
    "amount_minor": 5500,
    "currency": "USD"
  },
  "reason": "Damaged label",
  "manager_approval_id": "approval_1003"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| line_id | string · required |  |
| price | Money object `{ amount_minor: integer, currency: "USD" }` · required | New unit price. |
| reason | string · required |  |
| manager_approval_id | string · required | Action `price_override`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Approval missing/invalid |

---

### 2.19 `POST /pos/tickets/{ticketId}/hold`

**Hold ticket**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/hold` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/hold 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": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "held",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.20 `POST /pos/tickets/{ticketId}/resume`

**Resume ticket**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/resume` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/resume 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": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Not held |

---

### 2.21 `POST /pos/tickets/{ticketId}/void`

**Void ticket**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/void` |
| Auth | Staff + manager approval |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/void 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "reason": "Customer left",
  "manager_approval_id": "approval_1004"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| reason | string · required |  |
| manager_approval_id | string · required | Action `void`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "voided",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |

---

### 2.22 `POST /pos/tickets/{ticketId}/age-check`

**Record age check**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/age-check` |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/age-check 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>

{
  "result": "passed",
  "method": "physical_id_check"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| result | enum · required | `passed` \| `failed` \| `refused` \| `unavailable`. |
| method | enum · required | `physical_id_check`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ticket_id": "tkt_1001",
    "register_session_id": "rs_1001",
    "status": "open",
    "customer_id": null,
    "lines": [
      {
        "line_id": "tl_1",
        "product_id": "prod_101",
        "sku": "CAB-2023-750",
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "unit_price": {
          "amount_minor": 6275,
          "currency": "USD"
        },
        "override_price": null,
        "discount": {
          "amount_minor": 0,
          "currency": "USD"
        },
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "age_check_required": true,
    "age_check_result": "passed"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.23 `POST /pos/tickets/{ticketId}/payment`

**Take payment and complete sale**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/tickets/{ticketId}/payment` |
| Purpose | The single exactly-once sale command (D-04). Creates payment, tenders, sale/order, on_hand decrement, receipt and audit in one operation. `/pay` does not exist. |
| Auth | Staff `pos.sell` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/tickets/{ticketId}/payment 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "tenders": [
    {
      "type": "cash",
      "amount": {
        "amount_minor": 2000,
        "currency": "USD"
      },
      "cash_tendered": {
        "amount_minor": 2000,
        "currency": "USD"
      }
    },
    {
      "type": "card",
      "amount": {
        "amount_minor": 4777,
        "currency": "USD"
      },
      "terminal_reference": "term_txn_1001"
    }
  ]
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| tenders[].type | enum · required | `cash` \| `card` \| `gift_card` \| `store_credit`. |
| tenders[].amount | Money object `{ amount_minor: integer, currency: "USD" }` · required | Sum must equal ticket total. |
| tenders[].cash_tendered | Money · optional (cash) | For change calculation. |
| tenders[].terminal_reference | string · required (card) | From the card terminal. |
| tenders[].gift_card_code | string · required (gift_card) |  |
| tenders[].customer_id | string · required (store_credit) |  |

**Response — success (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "sale_id": "sale_1001",
    "order_id": "ord_pos_1001",
    "status": "completed",
    "total": {
      "amount_minor": 6777,
      "currency": "USD"
    },
    "tenders": [
      {
        "type": "cash",
        "amount": {
          "amount_minor": 2000,
          "currency": "USD"
        },
        "change": {
          "amount_minor": 0,
          "currency": "USD"
        }
      },
      {
        "type": "card",
        "amount": {
          "amount_minor": 4777,
          "currency": "USD"
        },
        "status": "captured"
      }
    ],
    "receipt_id": "rcpt_1001"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum | `completed` \| `payment_pending` (poll GET /pos/tickets/{id}; never re-charge). |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `REGISTER_SESSION_REQUIRED` | 409 | POS action requires an open register session. |
| `AGE_VERIFICATION_REQUIRED` | 422 | Age verification must be recorded before this step. |
| `PAYMENT_FAILED` | 402 | Provider declined or failed the payment. |
| `PAYMENT_RECONCILIATION_REQUIRED` | 409 | Unknown terminal outcome |
| `PAYMENT_ALREADY_CAPTURED` | 409 | The payment/order was already captured; returns the existing result where possible. |
| `INVENTORY_CONFLICT` | 409 | Requested quantity no longer available at the fulfilling location. |
| `VALIDATION_ERROR` | 422 | Tender sum ≠ total |

---

### 2.24 `GET /pos/sales/{saleId}/receipt`

**Receipt**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/pos/sales/{saleId}/receipt` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| reprint | boolean · optional | `true` records an audited reprint. |

**Request example**

```http
GET /api/v1/pos/sales/{saleId}/receipt HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "receipt_id": "rcpt_1001",
    "sale_id": "sale_1001",
    "location": "Orange Wine Albany",
    "cashier": "Jane D.",
    "lines": [
      {
        "name": "Example Cabernet 2023 750ml",
        "quantity": 1,
        "line_total": {
          "amount_minor": 6275,
          "currency": "USD"
        }
      }
    ],
    "totals": {
      "subtotal": {
        "amount_minor": 6275,
        "currency": "USD"
      },
      "discount": {
        "amount_minor": 0,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 502,
        "currency": "USD"
      },
      "total": {
        "amount_minor": 6777,
        "currency": "USD"
      }
    },
    "tenders": [
      {
        "type": "card",
        "last4": "4242",
        "amount": {
          "amount_minor": 4777,
          "currency": "USD"
        }
      }
    ],
    "printed_at": "2026-09-17T16:00:00Z",
    "reprint_count": 0
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.25 `POST /pos/sales/{saleId}/returns`

**In-store return**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/pos/sales/{saleId}/returns` |
| Purpose | Receive, inspect and refund in one staff action; D-24 policy still applies. |
| Auth | Staff `pos.refund` + manager approval |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/pos/sales/{saleId}/returns 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>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "items": [
    {
      "order_line_id": "ol_pos_1",
      "quantity": 1,
      "reason": "sound_no_defect",
      "disposition": "sellable_restock"
    }
  ],
  "refund_tender": "original_tender",
  "manager_approval_id": "approval_1006"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].reason | enum · required | Same list as online returns. |
| items[].disposition | enum · required | Inspection disposition. |
| refund_tender | enum · required | `original_tender` \| `store_credit`. |
| manager_approval_id | string · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "return_id": "ret_2101",
    "status": "completed",
    "refund_id": "ref_1101",
    "refund": {
      "final_refund": {
        "amount_minor": 5334,
        "currency": "USD"
      },
      "fee_lines": [
        {
          "fee_type": "restocking_fee",
          "amount": {
            "amount_minor": 941,
            "currency": "USD"
          }
        }
      ]
    },
    "inventory_movement_id": "mov_5001"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `RETURN_WINDOW_EXPIRED` | 422 | Outside the 30-day return window. |
| `REFUND_APPROVAL_REQUIRED` | 403 | Refund requires a valid manager approval. |
| `REFUND_ALREADY_ISSUED` | 409 | Refund for this scope was already issued. |

---
