# Orange Wine — API Part 02: Customer Account

Version 1.0 — 28 Sep 2026 · 11 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 |
|---|---|---|
| `AUTHENTICATION_REQUIRED` | 401 | No valid session, or the access token has expired and refresh failed. |
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `RESOURCE_NOT_FOUND` | 404 | Resource doesn't exist or isn't visible to the caller (never reveals existence). |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

## 2. Customer Account

Signed-in customer's own profile, addresses, preferences, orders and store credit. A customer can never read or change another customer's data (→ FORBIDDEN).

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/account` | Get my profile | CONFIRMED |
| 2.2 | PATCH | `/account` | Update my profile | CONFIRMED |
| 2.3 | GET | `/account/addresses` | List my addresses | CONFIRMED |
| 2.4 | POST | `/account/addresses` | Add address | CONFIRMED |
| 2.5 | PATCH | `/account/addresses/{addressId}` | Update address | CONFIRMED |
| 2.6 | DELETE | `/account/addresses/{addressId}` | Delete address | CONFIRMED |
| 2.7 | GET | `/account/preferences` | Get preferences | CONFIRMED |
| 2.8 | PATCH | `/account/preferences` | Update preferences | CONFIRMED |
| 2.9 | GET | `/account/orders` | My orders | CONFIRMED |
| 2.10 | GET | `/account/orders/{orderId}` | My order detail | CONFIRMED |
| 2.11 | GET | `/account/store-credit` | My store credit | CONFIRMED |

### 2.1 `GET /account`

**Get my profile**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "usr_101",
    "first_name": "John",
    "last_name": "Smith",
    "email": "customer@example.com",
    "phone": "+15185550100",
    "email_verified": true,
    "accepts_marketing": false,
    "created_at": "2026-09-01T10:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| phone | string \| null | E.164. |
| accepts_marketing | boolean | Marketing consent. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `AUTHENTICATION_REQUIRED` | 401 | Not signed in |
| `FORBIDDEN` | 403 | Signed in as staff |

---

### 2.2 `PATCH /account`

**Update my profile**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/account` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
PATCH /api/v1/account 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": "John",
  "last_name": "Smith",
  "phone": "+15185550199"
}
```

Email change is not supported by this endpoint (PROPOSED: separate verified flow).

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| first_name | string · optional |  |
| last_name | string · optional |  |
| phone | string \| null · optional | E.164. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "usr_101",
    "first_name": "John",
    "last_name": "Smith",
    "phone": "+15185550199"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (all) | object | Updated profile, same shape as GET /account. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Invalid field |

---

### 2.3 `GET /account/addresses`

**List my addresses**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/account/addresses` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/account/addresses 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": "addr_301",
        "label": "Home",
        "line1": "100 Main Street",
        "line2": "Suite 4",
        "city": "Albany",
        "state": "NY",
        "postal_code": "12201",
        "country": "US",
        "is_default_shipping": true,
        "is_default_billing": false,
        "validation_status": "unvalidated"
      }
    ],
    "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 |
|---|---|---|
| items[] | Address | See fields of POST /account/addresses response. |
| pagination.page | integer | Current page. |
| pagination.per_page | integer | Page size used. |
| pagination.total | integer | Total matching items. |
| pagination.last_page | integer | Last page number; 0 when there are no items. |

---

### 2.4 `POST /account/addresses`

**Add address**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/account/addresses` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/account/addresses 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>

{
  "label": "Home",
  "line1": "100 Main Street",
  "line2": "Suite 4",
  "city": "Albany",
  "state": "NY",
  "postal_code": "12201",
  "country": "US",
  "is_default_shipping": true,
  "is_default_billing": false
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| label | string · optional |  |
| line1 | string · required | Street address. |
| line2 | string · optional | Apartment, suite, unit. |
| city | string · required |  |
| state | string · required | 2-letter US state code, e.g. `NY`. |
| postal_code | string · required | 5-digit or ZIP+4. |
| country | string · required | ISO 3166-1 alpha-2. `US` only in V1. |
| is_default_shipping | boolean · optional | Setting `true` clears the flag on other addresses. |
| is_default_billing | boolean · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "addr_301",
    "label": "Home",
    "line1": "100 Main Street",
    "line2": "Suite 4",
    "city": "Albany",
    "state": "NY",
    "postal_code": "12201",
    "country": "US",
    "is_default_shipping": true,
    "is_default_billing": false,
    "validation_status": "unvalidated"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |
| label | string · optional | Customer's name for the address. |
| line1 | string · required | Street address. |
| line2 | string · optional | Apartment, suite, unit. |
| city | string · required |  |
| state | string · required | 2-letter US state code, e.g. `NY`. |
| postal_code | string · required | 5-digit or ZIP+4. |
| country | string · required | ISO 3166-1 alpha-2. `US` only in V1. |
| place_id | string · optional | Google Place ID once address validation is configured (gate). |
| validation_status | enum · response only | `unvalidated` \| `validated` \| `corrected` \| `failed` (PROPOSED value set). |
| is_default_shipping | boolean | Only one address may be `true`. |
| is_default_billing | boolean | Only one address may be `true`. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Missing/invalid field |

---

### 2.5 `PATCH /account/addresses/{addressId}`

**Update address**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/account/addresses/{addressId}` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
PATCH /api/v1/account/addresses/{addressId} 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>

{
  "line2": "Apt 9",
  "is_default_shipping": true
}
```

**Request keys**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "addr_301",
    "label": "Home",
    "line1": "100 Main Street",
    "line2": "Suite 4",
    "city": "Albany",
    "state": "NY",
    "postal_code": "12201",
    "country": "US",
    "is_default_shipping": true,
    "is_default_billing": false,
    "validation_status": "unvalidated"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| id | string |  |
| label | string · optional | Customer's name for the address. |
| line1 | string · required | Street address. |
| line2 | string · optional | Apartment, suite, unit. |
| city | string · required |  |
| state | string · required | 2-letter US state code, e.g. `NY`. |
| postal_code | string · required | 5-digit or ZIP+4. |
| country | string · required | ISO 3166-1 alpha-2. `US` only in V1. |
| place_id | string · optional | Google Place ID once address validation is configured (gate). |
| validation_status | enum · response only | `unvalidated` \| `validated` \| `corrected` \| `failed` (PROPOSED value set). |
| is_default_shipping | boolean | Only one address may be `true`. |
| is_default_billing | boolean | Only one address may be `true`. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Another customer's address |
| `RESOURCE_NOT_FOUND` | 404 | Unknown address |
| `VALIDATION_ERROR` | 422 | Invalid field |

---

### 2.6 `DELETE /account/addresses/{addressId}`

**Delete address**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/account/addresses/{addressId}` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

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

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| deleted | boolean | Always `true`. Past orders keep their address snapshot. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Another customer's address |
| `RESOURCE_NOT_FOUND` | 404 | Unknown address |

---

### 2.7 `GET /account/preferences`

**Get preferences**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/account/preferences` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "preferred_location_id": "loc_albany",
    "email_marketing": false,
    "sms_marketing": false,
    "sms_transactional": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| preferred_location_id | string \| null | Default store. |
| email_marketing | boolean | Consent recorded with timestamp server-side. |
| sms_marketing | boolean |  |
| sms_transactional | boolean | Order updates by SMS. |

---

### 2.8 `PATCH /account/preferences`

**Update preferences**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/account/preferences` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
PATCH /api/v1/account/preferences 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>

{
  "preferred_location_id": "loc_albany",
  "email_marketing": true
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (any preference field) | optional | See GET response. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "preferred_location_id": "loc_albany",
    "email_marketing": true,
    "sms_marketing": false,
    "sms_transactional": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Unknown location |

---

### 2.9 `GET /account/orders`

**My orders**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/account/orders` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| status | enum · optional | Order status filter. |
| order_type | enum · optional | `normal` \| `pre_arrival`. |
| 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/account/orders?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": "ord_5001",
        "confirmation_number": "OW-5001",
        "channel": "web",
        "order_type": "normal",
        "status": "paid",
        "payment_status": "captured",
        "fulfillment_status": "unassigned",
        "total": {
          "amount_minor": 13592,
          "currency": "USD"
        },
        "created_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"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].id | string |  |
| items[].confirmation_number | string | Customer-facing number, e.g. `OW-5001`. |
| items[].channel | enum | `web` \| `pos`. |
| items[].order_type | enum | `normal` \| `pre_arrival`. |
| items[].status | enum | `draft` \| `pending_payment` \| `payment_failed` \| `expired` \| `paid` \| `partially_fulfilled` \| `fulfilled` \| `cancelled` \| `partially_refunded` \| `refunded` (payment_failed/expired PROPOSED, D-30). |
| items[].payment_status | enum | `created` \| `pending` \| `authorized` \| `captured` \| `failed` \| `reversed` \| `partially_refunded` \| `refunded` \| `disputed`. |
| items[].fulfillment_status | enum | Aggregate of fulfillment groups: `unassigned` \| `in_progress` \| `partially_fulfilled` \| `fulfilled` \| `exception` \| `cancelled`. |
| items[].total | Money object `{ amount_minor: integer, currency: "USD" }` |  |
| pagination.page | integer | Current page. |
| pagination.per_page | integer | Page size used. |
| pagination.total | integer | Total matching items. |
| pagination.last_page | integer | Last page number; 0 when there are no items. |

---

### 2.10 `GET /account/orders/{orderId}`

**My order detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/account/orders/{orderId}` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "note": "Same body as GET /orders/{orderId}"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| (all) | Order | See GET /orders/{orderId}. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Not your order |
| `RESOURCE_NOT_FOUND` | 404 | Unknown order |

---

### 2.11 `GET /account/store-credit`

**My store credit**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/account/store-credit` |
| Auth | Customer |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "balance": {
      "amount_minor": 2500,
      "currency": "USD"
    },
    "entries": {
      "items": [
        {
          "id": "scl_11",
          "type": "issue",
          "amount": {
            "amount_minor": 2500,
            "currency": "USD"
          },
          "reason": "approved_return",
          "reference": "ref_1001",
          "created_at": "2026-09-20T10:00:00Z"
        }
      ],
      "pagination": {
        "page": 1,
        "per_page": 24,
        "total": 1,
        "last_page": 1
      }
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| balance | Money object `{ amount_minor: integer, currency: "USD" }` | Derived from the ledger. |
| entries.items[].type | enum | `issue` \| `redeem` \| `reversal` \| `adjust` \| `expire`. |
| entries.items[].amount | Money object `{ amount_minor: integer, currency: "USD" }` | Positive for credit, negative for debit. |
| entries.items[].reference | string \| null | Refund, order or approval reference. |

---
