# Storefront · Gift cards

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /storefront/giftcards/balance

**Check a gift card balance**

`operationId: GiftCardController_checkBalance`

Looks a card up by its **redemption code** and reports whether it can be spent and how much is on it. Separators and case in the code are ignored.

**A card that cannot be used is still a `200`.** The response carries `valid: false` with a `status` that says why — `not_found`, `invalid_pin`, `expired`, or the card's own status when it is inactive, suspended or cancelled. Branch on `valid` and `status`, not on the HTTP code.

For an unusable card `balance` is reported as `0` regardless of what the card holds. An active card found past its date is marked `expired` on the way through.

**Staff only** — the till and admin. Shoppers check a card through `GET /client/giftcards/balance`, which carries the per-address throttle.

#### Signature

```http
GET /storefront/giftcards/balance (code?: string, pin?: string) -> The card's spendability and balance
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The PIN is never echoed back and is never stored — only an HMAC of it.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | FORBIDDEN | This needs someone signed in to the business | Called with a site's app token or a customer token. | Shoppers use GET /client/giftcards/balance. |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /storefront/giftcards/redeem`
- `GET /client/giftcards/balance`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes | The redemption code. Not the serial. |
| `pin` | query | string | — | Required for physical cards. A wrong or missing PIN yields `status: "invalid_pin"`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The card's spendability and balance |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | This needs someone signed in to the business — Called with a site's app token or a customer token. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "valid": true,
  "balance": 35.5,
  "currency": "USD",
  "status": "active"
}
```

## GET /storefront/giftcards/wallet/status

**Which wallet passes this store can issue**

`operationId: GiftCardController_walletStatus`

`{ apple, google }` — true when the pass credentials are set on the gift card integration (Payment gateways › Gift card). Staff version of `GET /client/giftcards/wallet/status`.

#### Signature

```http
GET /storefront/giftcards/wallet/status () -> { apple, google }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/wallet/apple`
- `GET /storefront/giftcards/wallet/google`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { apple, google } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/wallet/apple

**Apple Wallet pass for a card**

`operationId: GiftCardController_applePass`

A signed `.pkpass` for the card with this **code** — balance, a QR of the code, expiry — sent as a file download (`Content-Type: application/vnd.apple.pkpass`). Staff only; shoppers use `GET /client/giftcards/wallet/apple`.

#### Signature

```http
GET /storefront/giftcards/wallet/apple (code?: string) -> The .pkpass file (binary)
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `501` | WALLET_NOT_SET_UP | Apple Wallet is not set up for this store | The pass credentials are not configured on the gift card integration. | Check GET …/wallet/status first and only offer the button when true. |
| `400` | CODE_REQUIRED | A gift card code is needed | No `code`. | — |
| `404` | GIFT_CARD_NOT_FOUND | Gift card not found | The code does not resolve. (This one is a real 404.) | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/wallet/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes | The redemption code, not the serial. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The .pkpass file (binary) |
| `400` | A gift card code is needed — No `code`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Gift card not found — The code does not resolve. (This one is a real 404.) |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `501` | Apple Wallet is not set up for this store — The pass credentials are not configured on the gift card integration. |

## GET /storefront/giftcards/wallet/google

**Google Wallet link for a card**

`operationId: GiftCardController_googlePass`

The "Save to Google Wallet" link for the card with this **code**. Staff only; shoppers use `GET /client/giftcards/wallet/google`.

#### Signature

```http
GET /storefront/giftcards/wallet/google (code?: string) -> { url }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `501` | WALLET_NOT_SET_UP | Google Wallet is not set up for this store | The pass credentials are not configured on the gift card integration. | Check GET …/wallet/status first and only offer the button when true. |
| `400` | CODE_REQUIRED | A gift card code is needed | No `code`. | — |
| `404` | GIFT_CARD_NOT_FOUND | Gift card not found | The code does not resolve. (This one is a real 404.) | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/wallet/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes | The redemption code, not the serial. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { url } |
| `400` | A gift card code is needed — No `code`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Gift card not found — The code does not resolve. (This one is a real 404.) |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `501` | Google Wallet is not set up for this store — The pass credentials are not configured on the gift card integration. |

## POST /storefront/giftcards/redeem

**Redeem a gift card**

`operationId: GiftCardController_redeem`

Spends value from a card against an order, under a per-card lock, and appends the movement to the card's `uses[]`. **Staff only** (the till); a shopper's checkout spends through `POST /client/giftcards/redeem`, which behaves the same.

**The amount is clamped** to the available balance and to the card's `maxUsePerTransaction`. Asking for more is not an error — the card pays what it can and `amountRedeemed` says how much. Always trust `amountRedeemed`, never the amount you asked for, and collect the difference another way.

The card is re-validated here, so an expired, suspended or inactive card is refused even if your balance check passed earlier. Restrictions are enforced when `context` describes the order.

**Idempotent on a key.** Send an `Idempotency-Key` header (or `idempotencyKey` in the body). A repeat with the same key returns the first result and moves no value — the site's checkout always sends one.

The response `transactionId` (GCT-…) is the **payment reference**: record the payment with `gateway: "giftcard"` and `ref: <transactionId>` and the ledger verifies it against the card before writing it as paid. One reference can back one payment.

#### Signature

```http
POST /storefront/giftcards/redeem (body) -> What was actually redeemed
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Two concurrent redemptions on one card are serialised; the balance can never be spent twice.
- `amountRedeemed` is the number your order should trust, never the amount you requested.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card not found | The code does not resolve. | Check the code. This is a `400`, not a `404`. |
| `409` | LOCKED | The gift card is being updated by another request. Try again. | Another redemption on the same card held the lock for longer than the retry window (~3s). | Retry with the same idempotency key. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/balance`
- `POST /storefront/giftcards/{serial}/refund`
- `POST /storefront/take-payment`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `idempotency-key` | header | string | yes |  |
| `Idempotency-Key` | header | string | — | Optional. Same key → same result, no second spend. |

### Request body

The card, the amount and the order.

```json
{
  "code": "YMFB-CDZ2-KDXS-KLM9",
  "amount": 14.5,
  "orderNumber": "A7K2M9QX4",
  "idempotencyKey": "9f1c-…"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | What was actually redeemed |
| `400` | Gift card not found — The code does not resolve. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `409` | The gift card is being updated by another request. Try again. — Another redemption on the same card held the lock for longer than the retry window (~3s). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "success": true,
  "amountRedeemed": 14.5,
  "remainingBalance": 21,
  "transactionId": "GCT-VB8C-SZ7F-E33V",
  "serial": "GC-7K2M-9QX4-H1P7",
  "currency": "USD"
}
```

## GET /storefront/giftcards

**List gift cards**

`operationId: GiftCardController_list`

Operator list with **server-side** search, filters, sort and paging. `search` matches serial, code, recipient name/email, buyer, the selling order and any order the card was spent on. Full records including balances and use history — an operator read.

#### Signature

```http
GET /storefront/giftcards (search?: string, status?: string, type?: string, batch?: string, campaign?: string, customerId?: string, expiringWithinDays?: integer, sort?: string, sortType?: string, page?: integer, pageSize?: integer) -> A page of gift cards
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/stats`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `status` | query | "inactive" \| "active" \| "used" \| "expired" \| "cancelled" \| "suspended" | — |  |
| `type` | query | "physical" \| "digital" \| "promotional" | — |  |
| `batch` | query | string | — |  |
| `campaign` | query | string | — | Promotional campaign name. |
| `search` | query | string | — |  |
| `customerId` | query | string | — |  |
| `expiringWithinDays` | query | integer | — | Live cards whose date falls within this many days. |
| `sort` | query | string | — | createdate, modifydate, data.balance, data.initialAmount, data.expirationDate, data.status, data.type, data.serial, data.recipientEmail |
| `sortType` | query | "asc" \| "desc" | — |  |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — | Max 200. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of gift cards |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards

**Create a gift card**

`operationId: GiftCardController_create`

Issues a single card. The serial and code are generated by the server; a caller may bring a `code` (a printed stock run) and it is checked for a clash.

Cards are `inactive` unless `activate: true` is sent — activate physical stock when it is sold. A physical card gets a PIN, which is **returned once on this response as `data.pin` and never again**; only its hash is stored.

**Issuing does not send anything.** To email/text the card to its recipient, call `POST /storefront/giftcards/{serial}/send` next.

Pick `type` by where the value came from: `digital`/`physical` for a card someone paid for (a liability), `promotional` for a free card — a goodwill or apology card, a giveaway — which is not a liability.

**Expiry follows the org policy** (`GET /storefront/giftcards/policy`) for purchased cards: no `expirationDate` takes the policy default (none = never expires), and a date sooner than the policy minimum is refused. Promotional cards may expire whenever.

**Cards sold as products are not issued here.** A product with `giftCard.enabled` on `sf_product` is a gift card: the product page asks for the value (`denominations[]`, or a custom amount within `minAmount`–`maxAmount` when `allowCustomAmount`), a recipient, a message and a delivery date, carried on the cart line as the options "Gift card amount", "Recipient name", "Recipient email", "Gift message", "Deliver on". Pricing honours "Gift card amount" and applies no tier, price list, benefit or discount to the line. When the order is paid — cart checkout, a pay link, or a POS settle — one active digital card is issued per unit (idempotent per order, line and unit), `purchaseOrderNumber` ties it to the order, and it is emailed at once or on the chosen date.

#### Signature

```http
POST /storefront/giftcards (body) -> The created card, with its serial and code (and `pin`, once, for a physical card)
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_VALUE | A gift card needs a value greater than zero | `initialAmount` is missing or not positive. | Send the value. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/giftcards/batch`
- `PUT /storefront/giftcards/{serial}/activate`
- `POST /storefront/giftcards/{serial}/send`
- `GET /storefront/giftcards/policy`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The card to issue.

```json
{
  "initialAmount": 50,
  "currency": "USD",
  "type": "digital",
  "activate": true,
  "recipientEmail": "ada@example.com",
  "recipientName": "Ada"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created card, with its serial and code (and `pin`, once, for a physical card) |
| `400` | A gift card needs a value greater than zero — `initialAmount` is missing or not positive. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/batch

**Create a batch of gift cards**

`operationId: GiftCardController_createBatch`

Issues `count` identical cards (1–1000) in one call, each with its own serial and code, under a shared `batch` id so they can be listed and managed together. Physical cards come back with their PINs, once, for the printer.

This is how physical stock and promotional runs are produced. Filter them later with `GET /storefront/giftcards?batch=…`.

#### Signature

```http
POST /storefront/giftcards/batch (body) -> The issued cards and their batch identifier
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The batch is written synchronously, one card at a time; 1000 is the ceiling per call.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | BATCH_SIZE | A batch is between 1 and 1000 cards | `count` is below 1 or above 1000. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards`
- `GET /storefront/giftcards/batches`
- `POST /storefront/giftcards/import`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

How many cards, and what they are worth.

```json
{
  "count": 100,
  "amount": 25,
  "currency": "USD",
  "type": "physical"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The issued cards and their batch identifier |
| `400` | A batch is between 1 and 1000 cards — `count` is below 1 or above 1000. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/stats

**Gift card statistics**

`operationId: GiftCardController_getStats`

The finance view, computed in the database: how much value is still owed to customers (`liability`), what was issued and redeemed, what expired unspent (`breakage`), what is about to expire, and the last 30 days. Promotional cards are reported separately and are never counted as a liability.

#### Signature

```http
GET /storefront/giftcards/stats () -> Aggregate gift card statistics
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Declared before `GET /storefront/giftcards/{serial}`, so `stats` always resolves as this route.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Aggregate gift card statistics |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/verify/{transactionId}

**Verify a redemption reference**

`operationId: GiftCardController_verify`

Whether a GCT-… reference is a real redemption, for how much, on which card. This is what the transaction ledger asks before it writes a `giftcard` payment as paid; exposed so an operator can chase a reference by hand.

#### Signature

```http
GET /storefront/giftcards/verify/{transactionId} (transactionId: string) -> The verification — always a `200`; read `isValid`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/giftcards/redeem`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `transactionId` | path | string | yes | The GCT-… reference from a redemption. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The verification — always a `200`; read `isValid` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/expire-due

**Expire due cards**

`operationId: GiftCardController_expireDue`

Flips every live card whose expiration date has passed to `expired` and posts the value left on purchased cards to the ledger as breakage income. A nightly job runs this per org (03:20); calling it is safe at any time and posts nothing twice.

#### Signature

```http
POST /storefront/giftcards/expire-due () -> What the sweep did
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | What the sweep did |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/import

**Issue cards from a list**

`operationId: GiftCardController_importCards`

One card per row — a customer list, an apology run, a promotion. Rows share the type, currency, expiry, message template and campaign given at the top level, and all land in one new batch (`BATCH-…`). With `send: true` each activated card is emailed and/or texted to its row.

**Bad rows are reported, not fatal**: a row without a positive amount, with a malformed email, or (when sending) with neither email nor phone comes back as `ok: false` with the `problem`, and the rest are issued. Cards are activated unless `activate: false`.

#### Signature

```http
POST /storefront/giftcards/import (body) -> The batch and one result per row
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMPTY_LIST | The list has no rows | `rows` is missing or empty. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/batches`
- `POST /storefront/giftcards/{serial}/send`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "type": "promotional",
  "campaign": "Spring apology",
  "send": true,
  "rows": [
    {
      "name": "Ada Lovelace",
      "email": "ada@example.com",
      "amount": 20,
      "message": "Sorry for the wait!"
    },
    {
      "name": "Grace",
      "phone": "+15555550123",
      "amount": "$15"
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The batch and one result per row |
| `400` | The list has no rows — `rows` is missing or empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "batch": "BATCH-7K2M",
  "issued": 1,
  "failed": 1,
  "results": [
    {
      "row": 1,
      "ok": true,
      "serial": "GC-7K2M-9QX4-H1P7",
      "to": "Ada Lovelace",
      "sent": [
        "email"
      ]
    },
    {
      "row": 2,
      "ok": false,
      "problem": "Amount must be greater than zero"
    }
  ]
}
```

## GET /storefront/giftcards/reconciliation

**Reconcile gift cards with the books**

`operationId: GiftCardController_reconciliation`

Compares the ledger's gift card liability account (the mapped `giftCardLiability` role, account 2030 by default) with the sum of balances on live purchased cards (active, inactive, suspended; promotional cards excluded).

How the books treat cards: the value of cards **sold** is booked to the liability account, not to sales; a payment made with a card debits that liability instead of cash; cards that expire unspent are released as breakage income by the nightly sweep. Promotional cards touch none of this.

`issuedWithoutSale` lists purchased cards issued by hand with no order behind them — their value was never booked as owed, which is the usual cause of a difference; `unexplained` is what remains once those are accounted for. `status` is `balanced`, `out_of_balance`, or `no_account` when the liability account does not exist, and `message` explains it in words.

#### Signature

```http
GET /storefront/giftcards/reconciliation () -> The comparison
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/stats`
- `GET /storefront/giftcards/insights`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The comparison |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/unusual

**Unusual gift card redemptions**

`operationId: GiftCardController_unusual`

Redemptions for someone to check, over the last `days`: a card **emptied within an hour** of being issued or activated (the usual shape of a stolen or leaked code), and a card **redeemed five or more times within a day**. Newest first, at most 100 flags.

#### Signature

```http
GET /storefront/giftcards/unusual (days?: integer) -> The flags
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `days` | query | integer | — | 1–365. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The flags |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/insights

**Gift card insights**

`operationId: GiftCardController_insights`

The gift card dashboard over the last `months`: liability aging (0–30 days … over 2 years), outstanding by batch (top 10), redemption by channel, monthly issued vs redeemed/refunded/reloaded, purchased vs promotional totals, breakage (expired value) for each, and promotional campaigns with their redemption rate.

#### Signature

```http
GET /storefront/giftcards/insights (months?: integer) -> The dashboard figures
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/stats`
- `GET /storefront/giftcards/reconciliation`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `months` | query | integer | — | 1–36. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The dashboard figures |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/batches

**List gift card batches**

`operationId: GiftCardController_listBatches`

Each batch id with its name, note, type, size, total value, balance left and cards by status — computed from the cards, newest first. `search` matches the batch id, card name or batch name.

#### Signature

```http
GET /storefront/giftcards/batches (search?: string, page?: integer, pageSize?: integer) -> A page of batches
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/batches/{batch}/cards`
- `POST /storefront/giftcards/batches/{batch}/{action}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `search` | query | string | — |  |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — | Max 100. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of batches |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/policy

**Get the gift card expiry policy**

`operationId: GiftCardController_getPolicy`

How long a **purchased** card lasts when no date is given (`defaultExpiresAfterDays`, null = never expires) and the earliest date an operator may set (`minimumExpiryDays`, default 1825 days / 5 years). Promotional cards are not bound by it.

#### Signature

```http
GET /storefront/giftcards/policy () -> The policy
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/giftcards/policy`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The policy |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "defaultExpiresAfterDays": null,
  "minimumExpiryDays": 1825
}
```

## POST /storefront/giftcards/policy

**Set the gift card expiry policy**

`operationId: GiftCardController_setPolicy`

Omitted fields keep their current value. `defaultExpiresAfterDays` null or 0 means purchased cards never expire by default. A default shorter than the minimum is refused.

#### Signature

```http
POST /storefront/giftcards/policy (body) -> The policy now in force
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DEFAULT_BELOW_MINIMUM | The default expiry (<n> days) is shorter than the minimum (<m> days) | `defaultExpiresAfterDays` is set and below `minimumExpiryDays`. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "defaultExpiresAfterDays": 1825,
  "minimumExpiryDays": 1825
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The policy now in force |
| `400` | The default expiry (<n> days) is shorter than the minimum (<m> days) — `defaultExpiresAfterDays` is set and below `minimumExpiryDays`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/batches/{batch}/details

**Rename a batch or add a note**

`operationId: GiftCardController_updateBatch`

Writes `name` (as `batchName`) and/or `note` onto every card in the batch. An empty string clears it.

#### Signature

```http
POST /storefront/giftcards/batches/{batch}/details (batch: string, body) -> How many cards were updated
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | NOTHING_TO_CHANGE | Nothing to change | Neither `name` nor `note` was sent. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `batch` | path | string | yes | Batch id (`BATCH-…`). |

### Request body

```json
{
  "name": "Holiday stock 2026",
  "note": "Printed run, box 3"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | How many cards were updated |
| `400` | Nothing to change — Neither `name` nor `note` was sent. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/batches/{batch}/cards

**Cards in a batch**

`operationId: GiftCardController_batchCards`

Every card in the batch, oldest first, for re-export. Includes the redemption codes — **never the PINs**.

#### Signature

```http
GET /storefront/giftcards/batches/{batch}/cards (batch: string) -> The batch's cards
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `batch` | path | string | yes | Batch id (`BATCH-…`). |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The batch's cards |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/batches/{batch}/{action}

**Block, unblock or cancel a whole batch**

`operationId: GiftCardController_batchAction`

`action` is `suspend`, `reactivate` or `cancel` — lost stock, leaked codes. Each card goes through the same guarded action as a single card, so a card with a redemption on it is never cancelled. Cards already in the target state (or, for reactivate, not blocked) are skipped with the reason. A `reason` is required for suspend and cancel.

#### Signature

```http
POST /storefront/giftcards/batches/{batch}/{action} (batch: string, action: string, body) -> What was done and what was skipped
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | UNKNOWN_ACTION | Unknown batch action | `action` is not suspend, reactivate or cancel. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `batch` | path | string | yes | Batch id (`BATCH-…`). |
| `action` | path | "suspend" \| "reactivate" \| "cancel" | yes | suspend, reactivate or cancel. |

### Request body

```json
{
  "reason": "Box of cards reported stolen"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | What was done and what was skipped |
| `400` | Unknown batch action — `action` is not suspend, reactivate or cancel. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/activity

**Gift card activity**

`operationId: GiftCardController_activity`

Every movement on every card — redemptions, refunds, reloads, adjustments, transfers — newest first, filtered and paged in the database, with totals for the filtered set by kind and (for redemptions) by channel. `search` matches serial, holder name/email/phone, order number, redeemer and GCT reference.

#### Signature

```http
GET /storefront/giftcards/activity (kind?: string, channel?: string, from?: string, to?: string, batch?: string, search?: string, page?: integer, pageSize?: integer) -> A page of movements with summary totals
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/giftcards/{serial}/timeline`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `kind` | query | "redeem" \| "refund" \| "reload" \| "adjust" \| "transfer_in" \| "transfer_out" | — |  |
| `channel` | query | string | — | checkout, pos, invoice, admin, gateway. |
| `from` | query | string | — | Date, YYYY-MM-DD. |
| `to` | query | string | — | Date, YYYY-MM-DD. |
| `batch` | query | string | — |  |
| `search` | query | string | — |  |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — | Max 200. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of movements with summary totals |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/link-owners

**Link cards to customers**

`operationId: GiftCardController_linkOwners`

Backfill: every card with no `customerId` whose recipient email (or buyer email) or phone matches an existing customer is linked to that customer. Never creates customers. Looks at up to 5000 unowned cards per call.

#### Signature

```http
POST /storefront/giftcards/link-owners () -> How many were checked and linked
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | How many were checked and linked |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/{serial}

**Get a gift card**

`operationId: GiftCardController_getBySerial`

One card by serial, with its balance and full ledger. Exposes the redemption `code`, so it is staff only — shoppers use `GET /client/giftcards/balance`, or `GET /client/giftcards/me` for their own cards.

#### Signature

```http
GET /storefront/giftcards/{serial} (serial: string) -> The gift card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The gift card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/giftcards/{serial}/timeline

**A card's timeline**

`operationId: GiftCardController_timeline`

One card's whole story, newest first: money movements (`type: money`), sends (`type: sent`) and everything else — status, holder and template changes (`type: event`).

#### Signature

```http
GET /storefront/giftcards/{serial}/timeline (serial: string) -> The timeline
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The timeline |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/reissue

**Reissue a card**

`operationId: GiftCardController_reissue`

Replaces a damaged, lost or leaked card: a new card is issued to the same holder with the same rules and expiry, the whole balance moves onto it, and the old card is voided — both ledgers record the move. The new card is then sent to the holder; `delivery` says how that went.

#### Signature

```http
POST /storefront/giftcards/{serial}/reissue (serial: string, body) -> The voided card, the new one and the send result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

```json
{
  "reason": "Code shared publicly"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The voided card, the new one and the send result |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/giftcards/{serial}/activate

**Activate a gift card**

`operationId: GiftCardController_activate`

Brings an inactive card into service. This is the step that arms physical stock once it is sold. A card already in another state is refused rather than treated as a no-op.

#### Signature

```http
PUT /storefront/giftcards/{serial}/activate (serial: string) -> The activated card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/giftcards/{serial}/suspend`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The activated card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/giftcards/{serial}/suspend

**Suspend a gift card**

`operationId: GiftCardController_suspend`

Reversible block — the response to a card reported lost or suspected stolen. The balance is untouched and redemption fails while it is suspended. Use this rather than cancel for anything you might undo, and for any card that has been spent against, since those cannot be cancelled.

#### Signature

```http
PUT /storefront/giftcards/{serial}/suspend (serial: string, body) -> The suspended card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/giftcards/{serial}/reactivate`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

Why.

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The suspended card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/giftcards/{serial}/reactivate

**Reactivate a suspended gift card**

`operationId: GiftCardController_reactivate`

Lifts a suspension. The card returns to the state it had before — active, or inactive if it had never been activated — with its balance intact.

#### Signature

```http
PUT /storefront/giftcards/{serial}/reactivate (serial: string) -> The reactivated card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The reactivated card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/giftcards/{serial}/cancel

**Cancel a gift card**

`operationId: GiftCardController_cancel`

Voids an **unused** card permanently. **A card with a redemption on it cannot be cancelled** — its ledger is a record of value already given to a customer. Suspend it instead.

#### Signature

```http
PUT /storefront/giftcards/{serial}/cancel (serial: string, body) -> The cancelled card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

Why.

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The cancelled card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/refund

**Refund value onto a gift card**

`operationId: GiftCardController_refund`

Adds balance back, recording it against an order — a returned item refunded to store credit, or a redemption reversed. A fully-used card becomes active again. Send an `Idempotency-Key` header (or `idempotencyKey` in the body). A repeat with the same key returns the first result and moves no value — the site's checkout always sends one.

#### Signature

```http
POST /storefront/giftcards/{serial}/refund (serial: string, body) -> The card with its increased balance
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The amount is not checked against what was originally redeemed; a card can hold more than its initial value.
- Returns completed with `refundMethod: giftcard` call this themselves — see `POST /storefront/returns/{rma}/complete`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `idempotency-key` | header | string | yes |  |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |
| `Idempotency-Key` | header | string | — | Optional. |

### Request body

How much, against what.

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The card with its increased balance |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/resend

**Resend a gift card email**

`operationId: GiftCardController_resend`

Emails the card again — to its recipient, or to the address given. The operator's answer to "it never arrived". The email carries the code, never a PIN.

#### Signature

```http
POST /storefront/giftcards/{serial}/resend (serial: string, body) -> Whether it went, and where
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

Optional other address.

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether it went, and where |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/send

**Send a gift card**

`operationId: GiftCardController_send`

Sends the card by email and by SMS through the platform notification channels, using the card's message template (or `gift-card-issued`) and its SMS sibling. An `email`/`phone` given here replaces the one on the card (`to` may be either); with neither on the card, the buyer's email is used. Every send is recorded on the card.

**Not being able to send is a `201` with `delivered: false`** and a `reason` (e.g. "no email or phone on the card") — check it.

#### Signature

```http
POST /storefront/giftcards/{serial}/send (serial: string, body) -> What was sent where
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/giftcards/{serial}/resend`
- `POST /storefront/giftcards/{serial}/template`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | What was sent where |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/owner

**Set the holder of a card**

`operationId: GiftCardController_setOwner`

Pick a customer by `customerId` — their name, email and phone come with them (anything also given in the body wins) — or give `name`/`email`/`phone`, linked to a matching customer when one exists. Recorded as a holder-changed event.

#### Signature

```http
POST /storefront/giftcards/{serial}/owner (serial: string, body) -> The card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

```json
{
  "customerId": "66f1c0ffee12"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/template

**Set the message template a card is sent with**

`operationId: GiftCardController_setTemplate`

The email template used by `send` (its SMS sibling is used for texts). An empty or missing `messageTemplate` clears it back to the platform default.

#### Signature

```http
POST /storefront/giftcards/{serial}/template (serial: string, body) -> The card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/{serial}/adjust

**Adjust a gift card balance**

`operationId: GiftCardController_adjust`

Operator correction, up or down, always with a reason, recorded on the card's ledger as `kind: adjust`. Cannot take the balance below zero.

#### Signature

```http
POST /storefront/giftcards/{serial}/adjust (serial: string, body) -> The adjusted card
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | GIFT_CARD_NOT_FOUND | Gift card <serial> not found | No gift card in the org has that serial. | Check the serial with `GET /storefront/giftcards?search=`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes | Gift card **serial number**, not the redemption code. Case-insensitive. |

### Request body

Signed amount and why.

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The adjusted card |
| `400` | Gift card <serial> not found — No gift card in the org has that serial. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/giftcards/transfer

**Transfer balance between gift cards**

`operationId: GiftCardController_transfer`

Moves value from one active card to another of the same currency — consolidating balances, or reissuing a damaged card onto a fresh one. Unlike redemption the amount is **not** clamped: an over-ask is refused, not partially performed. Both cards are locked for the move.

#### Signature

```http
POST /storefront/giftcards/transfer (body) -> Both cards after the move
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Uses serials, not redemption codes — the opposite of `redeem` and `balance`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | SOURCE_NOT_FOUND | Source gift card <serial> not found | `fromSerial` does not resolve. | Check the serial. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

From, to, how much.

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Both cards after the move |
| `400` | Source gift card <serial> not found — `fromSerial` does not resolve. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /client/giftcards/balance

**Look a card up by its code (shopper)**

`operationId: GiftCardClientController_balance`

The shopper-side balance check: whether the card can be spent, what is left, and what is written on it — never an address or the PIN. Same answers as the staff check (`valid: false` with a `status` for an unusable card).

**Throttled**: 30 checks per client address per 10 minutes, then `429`. A balance check is a code-existence oracle, and this is what keeps it from being an enumeration tool.

#### Signature

```http
GET /client/giftcards/balance () -> The balance check
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `429` | TOO_MANY_REQUESTS | Too many balance checks. Try again in a few minutes. | More than 30 checks from one address in 10 minutes. | Wait, or use GET /client/giftcards/me, which lists the customer's own cards without a lookup. |

Plus the standard platform errors: `401`, `403`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes |  |
| `pin` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The balance check |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too many balance checks. Try again in a few minutes. — More than 30 checks from one address in 10 minutes. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /client/giftcards/redeem

**Spend a card at checkout (shopper)**

`operationId: GiftCardClientController_redeem`

Clamped to the balance and the per-transaction cap; `amountRedeemed` is the figure to trust. Send an idempotency key so a retry cannot spend twice. `usedBy` is the signed-in customer, else `guest`.

#### Signature

```http
POST /client/giftcards/redeem () -> The redemption, with its GCT reference
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `idempotency-key` | header | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The redemption, with its GCT reference |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /client/giftcards/wallet/status

**Which wallet passes this store can issue**

`operationId: GiftCardClientController_walletStatus`

`{ apple, google }` — true when the store has put its pass credentials on the gift card integration (Payment gateways › Gift card). Show an Add-to-Wallet button only when true. `GET /client/giftcards/wallet/apple?code=` returns the signed `.pkpass`; `GET /client/giftcards/wallet/google?code=` returns `{ url }`, the Save to Google Wallet link. Both are throttled like the balance check; `501` until set up.

#### Signature

```http
GET /client/giftcards/wallet/status () -> { apple, google }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { apple, google } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /client/giftcards/wallet/apple

**Apple Wallet pass for my card**

`operationId: GiftCardClientController_applePass`

The signed `.pkpass` for the card with this code, as a file download. Counts against the same per-address throttle as the balance check (30 checks per 10 minutes).

#### Signature

```http
GET /client/giftcards/wallet/apple (code?: string) -> The .pkpass file (binary)
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `501` | WALLET_NOT_SET_UP | Apple Wallet is not set up for this store | The pass credentials are not configured on the gift card integration. | Check GET …/wallet/status first and only offer the button when true. |
| `400` | CODE_REQUIRED | A gift card code is needed | No `code`. | — |
| `404` | GIFT_CARD_NOT_FOUND | Gift card not found | The code does not resolve. (This one is a real 404.) | — |
| `429` | TOO_MANY_CHECKS | Too many gift card checks. Try again in a few minutes. | More than 30 checks from one address in 10 minutes (shared with the balance check). | — |

Plus the standard platform errors: `401`, `403`, `500`.

#### See also

- `GET /client/giftcards/wallet/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes | The redemption code, not the serial. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The .pkpass file (binary) |
| `400` | A gift card code is needed — No `code`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Gift card not found — The code does not resolve. (This one is a real 404.) |
| `429` | Too many gift card checks. Try again in a few minutes. — More than 30 checks from one address in 10 minutes (shared with the balance check). |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `501` | Apple Wallet is not set up for this store — The pass credentials are not configured on the gift card integration. |

## GET /client/giftcards/wallet/google

**Google Wallet link for my card**

`operationId: GiftCardClientController_googlePass`

The "Save to Google Wallet" link for the card with this code. Counts against the same per-address throttle as the balance check.

#### Signature

```http
GET /client/giftcards/wallet/google (code?: string) -> { url }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `501` | WALLET_NOT_SET_UP | Google Wallet is not set up for this store | The pass credentials are not configured on the gift card integration. | Check GET …/wallet/status first and only offer the button when true. |
| `400` | CODE_REQUIRED | A gift card code is needed | No `code`. | — |
| `404` | GIFT_CARD_NOT_FOUND | Gift card not found | The code does not resolve. (This one is a real 404.) | — |
| `429` | TOO_MANY_CHECKS | Too many gift card checks. Try again in a few minutes. | More than 30 checks from one address in 10 minutes (shared with the balance check). | — |

Plus the standard platform errors: `401`, `403`, `500`.

#### See also

- `GET /client/giftcards/wallet/status`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `code` | query | string | yes | The redemption code, not the serial. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { url } |
| `400` | A gift card code is needed — No `code`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Gift card not found — The code does not resolve. (This one is a real 404.) |
| `429` | Too many gift card checks. Try again in a few minutes. — More than 30 checks from one address in 10 minutes (shared with the balance check). |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `501` | Google Wallet is not set up for this store — The pass credentials are not configured on the gift card integration. |

## GET /client/giftcards/me

**My gift cards**

`operationId: GiftCardClientController_myCards`

The signed-in customer's cards — held, bought, or sent to them — held ones first. A card they gave away shows only its last four. Includes `usableBalance` and the store's top-up product. Never the PIN. The account page and the checkout's "your gift cards" picker read this.

#### Signature

```http
GET /client/giftcards/me () -> The customer's cards
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The customer's cards |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /client/giftcards/me/{serial}/share

**Send part of my card to someone**

`operationId: GiftCardClientController_share`

Moves `amount` from the holder's card onto a new card for someone else (`name`, `email` and/or `phone`, `message`) and sends it to them. Only the card's holder may.

#### Signature

```http
POST /client/giftcards/me/{serial}/share () -> The new card and what is left on the holder's
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `serial` | path | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The new card and what is left on the holder's |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

