# Orange Wine — API Part 09: Gift Cards and Store Credit

Version 1.0 — 28 Sep 2026 · 8 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 |
|---|---|---|
| `INVALID_STATE_TRANSITION` | 409 | Named command not allowed from the resource's current state. |
| `RATE_LIMITED` | 429 | Too many requests. `Retry-After` header set. Limits are configured later. |
| `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. |

### Permissions used in this part

| Permission | Grants | Endpoints |
|---|---|---|
| `gift_cards.manage` | Gift cards and store credit | 3 |

### Endpoints requiring Idempotency-Key

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

## 2. Gift Cards and Store Credit

Append-only ledgers; balances are always derived (D-13, D-14). Legacy balances are not redeemable until reconciled (D-15, D-16).

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | POST | `/dashboard/gift-cards` | Issue gift card | CONFIRMED |
| 2.2 | GET | `/dashboard/gift-cards/{giftCardId}` | Gift card detail | CONFIRMED |
| 2.3 | POST | `/dashboard/gift-cards/{giftCardId}/activate` | Activate | CONFIRMED |
| 2.4 | POST | `/dashboard/gift-cards/{giftCardId}/adjust` | Adjust balance | CONFIRMED |
| 2.5 | POST | `/dashboard/gift-cards/{giftCardId}/deactivate` | Deactivate | CONFIRMED |
| 2.6 | GET | `/gift-cards/{code}/balance` | Check balance | CONFIRMED |
| 2.7 | GET | `/dashboard/store-credit/{customerId}` | Customer store credit | CONFIRMED |
| 2.8 | POST | `/dashboard/store-credit/{customerId}/adjust` | Adjust store credit | CONFIRMED |

### 2.1 `POST /dashboard/gift-cards`

**Issue gift card**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/gift-cards` |
| Auth | Staff `gift_cards.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Request example**

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

{
  "type": "digital",
  "initial_value": {
    "amount_minor": 5000,
    "currency": "USD"
  },
  "recipient_email": "friend@example.com",
  "message": "Happy birthday!"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| type | enum · required | `physical` \| `digital`. |
| initial_value | Money object `{ amount_minor: integer, currency: "USD" }` · required |  |
| recipient_email | string · required for digital |  |
| physical_code | string · required for physical | Pre-printed card code. |
| message | string · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "gift_card_id": "gc_101",
    "masked_code": "****-****-1234",
    "type": "digital",
    "status": "inactive",
    "balance": {
      "amount_minor": 5000,
      "currency": "USD"
    },
    "expires_at": null,
    "recipient_email": "friend@example.com",
    "code": "OWGC-8K2M-4J9P-1234"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| type | enum | `physical` \| `digital`. |
| status | enum | `inactive` \| `active` \| `deactivated` \| `expired`. |
| balance | Money object `{ amount_minor: integer, currency: "USD" }` | Derived from the ledger. |
| masked_code | string | Full code shown once at issue, never again. |
| code | string | Full code — returned only in this response. |

---

### 2.2 `GET /dashboard/gift-cards/{giftCardId}`

**Gift card detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/gift-cards/{giftCardId}` |
| Auth | Staff `gift_cards.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "gift_card_id": "gc_101",
    "masked_code": "****-****-1234",
    "type": "digital",
    "status": "active",
    "balance": {
      "amount_minor": 5000,
      "currency": "USD"
    },
    "expires_at": null,
    "recipient_email": "friend@example.com",
    "ledger": [
      {
        "id": "gcl_1",
        "type": "issue",
        "amount": {
          "amount_minor": 5000,
          "currency": "USD"
        },
        "created_at": "2026-09-17T16:00:00Z"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| type | enum | `physical` \| `digital`. |
| status | enum | `inactive` \| `active` \| `deactivated` \| `expired`. |
| balance | Money object `{ amount_minor: integer, currency: "USD" }` | Derived from the ledger. |
| masked_code | string | Full code shown once at issue, never again. |
| ledger[].type | enum | `issue` \| `activate` \| `redeem` \| `refund` \| `reversal` \| `adjust` \| `expire`. |

---

### 2.3 `POST /dashboard/gift-cards/{giftCardId}/activate`

**Activate**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/gift-cards/{giftCardId}/activate` |
| Auth | Staff `gift_cards.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/dashboard/gift-cards/{giftCardId}/activate HTTP/1.1
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
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "gift_card_id": "gc_101",
    "masked_code": "****-****-1234",
    "type": "digital",
    "status": "active",
    "balance": {
      "amount_minor": 5000,
      "currency": "USD"
    },
    "expires_at": null,
    "recipient_email": "friend@example.com"
  },
  "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.4 `POST /dashboard/gift-cards/{giftCardId}/adjust`

**Adjust balance**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/gift-cards/{giftCardId}/adjust` |
| Auth | Manager |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/dashboard/gift-cards/{giftCardId}/adjust 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

{
  "amount": {
    "amount_minor": -1000,
    "currency": "USD"
  },
  "reason": "Customer service correction",
  "manager_approval_id": "approval_1007"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| amount | Money object `{ amount_minor: integer, currency: "USD" }` · required | Signed. |
| reason | string · required |  |
| manager_approval_id | string · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "gift_card_id": "gc_101",
    "masked_code": "****-****-1234",
    "type": "digital",
    "status": "active",
    "balance": {
      "amount_minor": 4000,
      "currency": "USD"
    },
    "expires_at": null,
    "recipient_email": "friend@example.com"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Balance would go negative |

---

### 2.5 `POST /dashboard/gift-cards/{giftCardId}/deactivate`

**Deactivate**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/gift-cards/{giftCardId}/deactivate` |
| Auth | Manager |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/dashboard/gift-cards/{giftCardId}/deactivate 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": "Reported stolen",
  "manager_approval_id": "approval_1008"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| reason | string · required |  |
| manager_approval_id | string · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "gift_card_id": "gc_101",
    "masked_code": "****-****-1234",
    "type": "digital",
    "status": "deactivated",
    "balance": {
      "amount_minor": 5000,
      "currency": "USD"
    },
    "expires_at": null,
    "recipient_email": "friend@example.com"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.6 `GET /gift-cards/{code}/balance`

**Check balance**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/gift-cards/{code}/balance` |
| Auth | Public |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | Yes |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| code | string | Full gift card code. |

**Request example**

```http
GET /api/v1/gift-cards/{code}/balance HTTP/1.1
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "status": "active",
    "balance": {
      "amount_minor": 4000,
      "currency": "USD"
    },
    "expires_at": null
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `RESOURCE_NOT_FOUND` | 404 | Generic — never confirms whether a code exists |
| `RATE_LIMITED` | 429 | Too many requests. `Retry-After` header set. Limits are configured later. |

---

### 2.7 `GET /dashboard/store-credit/{customerId}`

**Customer store credit**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/store-credit/{customerId}` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

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

**Request example**

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

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "customer_id": "usr_101",
    "balance": {
      "amount_minor": 2500,
      "currency": "USD"
    },
    "entries": {
      "items": [
        {
          "id": "scl_11",
          "type": "issue",
          "amount": {
            "amount_minor": 2500,
            "currency": "USD"
          },
          "reason": "approved_return",
          "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 |
|---|---|---|
| entries.items[].type | enum | `issue` \| `redeem` \| `reversal` \| `adjust` \| `expire`. |

---

### 2.8 `POST /dashboard/store-credit/{customerId}/adjust`

**Adjust store credit**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/store-credit/{customerId}/adjust` |
| Auth | Manager |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

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

**Request example**

```http
POST /api/v1/dashboard/store-credit/{customerId}/adjust 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

{
  "amount": {
    "amount_minor": 2500,
    "currency": "USD"
  },
  "direction": "credit",
  "reason": "approved_return",
  "manager_approval_id": "approval_1009"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| amount | Money object `{ amount_minor: integer, currency: "USD" }` · required | Positive. |
| direction | enum · required | `credit` \| `debit`. |
| reason | string · required |  |
| manager_approval_id | string · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "ledger_entry_id": "scl_12",
    "customer_id": "usr_101",
    "balance": {
      "amount_minor": 5000,
      "currency": "USD"
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Debit exceeds balance |

---
