# Storefront · Discounts

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /storefront/discounts/validate

**Validate a discount code**

`operationId: DiscountController_validate`

Checks whether a code can be used by the calling customer for a given cart, and says precisely why not when it cannot.

**A rejection is a `200`, not an error.** The response carries `valid: false` with a machine-readable `reason` and a message written for the shopper. Branch on `reason`; the `message` text is for display and may change.

| `reason` | Meaning |
| --- | --- |
| `not_found` | No discount in the org has that code. |
| `inactive` | The discount exists but its status is not active. |
| `not_started` | The discount's start date is in the future. |
| `expired` | The discount's end date has passed. |
| `usage_limit` | The discount has been redeemed as many times as it allows. |
| `customer_group` | The customer is not in a group the discount targets. |
| `customer_group_excluded` | The customer is in a group the discount excludes. |
| `customer_not_allowed` | The discount names specific customers and this is not one of them. |
| `customer_excluded` | The discount excludes this specific customer. |
| `email_required` | The discount needs an email to evaluate and none was available. |
| `first_order_only` | The discount is for first-time customers and this one has ordered before. |
| `min_cart_value` | The cart subtotal is below the discount's minimum. |
| `min_cart_items` | The cart has fewer items than the discount requires. |
| `required_products` | The cart does not contain the products the discount requires. |

The email used for first-order and email-gated checks comes from the signed-in customer when there is one, falling back to the `email` in the body.

This validates only — it records no usage and changes nothing.

#### Signature

```http
POST /storefront/discounts/validate (body) -> Whether the code applies, and why not when it does not
```

#### Access

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

#### Notes

- The four `customer_*` reasons all share the message "Discount not available for your account" — they are only distinguishable by `reason`.
- Validation does not reserve or record anything. A code valid here can still be exhausted by someone else before checkout.

#### Errors

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

#### See also

- `POST /storefront/discounts/apply`
- `POST /storefront/discounts/{identifier}/usage`

### 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 code and the cart to test it against.

```json
{
  "code": "SUMMER20",
  "subtotal": 129.99
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the code applies, and why not when it does not |
| `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
{
  "valid": true,
  "discount": {
    "data": {
      "code": "SUMMER20",
      "type": "percent",
      "value": 20
    }
  }
}
```

## GET /storefront/discounts/promotions/active

**List active promotions for display**

`operationId: DiscountController_getActivePromotions`

A shopper-safe list of the active, **code-based** promotions — the "current offers" strip on a storefront.

The response is deliberately a projection, not the discount records: only `code`, `name`, `description`, `type`, `value`, `minCartValue` and `endDate` are exposed, so targeting rules, usage limits and customer restrictions are never leaked to the browser.

Auto-apply discounts are excluded — they have no code to advertise.

#### Signature

```http
GET /storefront/discounts/promotions/active () -> Active coded promotions, projected for display
```

#### Access

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

#### Notes

- Listing a promotion here does not mean a given shopper can use it — group and customer targeting still apply. Validate before promising it.
- Unpaged: every active coded promotion is returned.

#### Errors

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

#### See also

- `POST /storefront/discounts/validate`

### 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` | Active coded promotions, projected for display |
| `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/discounts/automatic

**Get automatic discounts for a cart**

`operationId: DiscountController_getAutomaticDiscounts`

Returns the code-less discounts that would apply to this cart for the calling customer, without pricing the cart. Use it to show "you are getting 10% off" before the shopper reaches checkout.

#### Signature

```http
POST /storefront/discounts/automatic (body) -> The automatic discounts that qualify
```

#### Access

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

#### Notes

- Only discounts with `autoApply: true` are considered. Coded promotions never appear here.

#### Errors

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

#### See also

- `GET /storefront/discounts/promotions/active`

### 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 cart to evaluate.

```json
{
  "subtotal": 129.99,
  "productItems": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2,
      "price": 12
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The automatic discounts that qualify |
| `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/discounts/apply

**Apply a coupon and price the cart**

`operationId: DiscountController_applyCoupon`

Prices a cart with a coupon applied, together with every automatic discount that qualifies. This is the call a checkout page makes when the shopper enters a code.

The code may be sent as either `couponCode` or `code` — `couponCode` wins when both are present.

An invalid code does not fail the request: the cart is priced without it and the reason is reported in the response, so the page can show the total *and* explain why the code did not take.

#### Signature

```http
POST /storefront/discounts/apply (body) -> The priced cart, with applied discounts and any coupon rejection reason
```

#### Access

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

#### Notes

- Pricing only — nothing is redeemed. Record the redemption at order time with `POST /storefront/discounts/{identifier}/usage`.
- Identical to `POST /storefront/discounts/calculate` except that this one accepts the `code` alias.

#### Errors

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

#### See also

- `POST /storefront/discounts/calculate`
- `POST /storefront/discounts/validate`

### 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 cart and the coupon.

```json
{
  "couponCode": "SUMMER20",
  "productItems": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2,
      "price": 12
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The priced cart, with applied discounts and any coupon rejection reason |
| `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/discounts/calculate

**Calculate a cart with all discounts**

`operationId: DiscountController_calculateCart`

Prices a cart with every applicable discount — automatic ones always, plus `couponCode` when supplied.

This is the same calculation `POST /storefront/pricing/calculate-cart` performs; that endpoint delegates here so cart discounting has one implementation. The only difference from `apply` is that this one does not accept the legacy `code` alias.

#### Signature

```http
POST /storefront/discounts/calculate (body) -> The priced cart with all discounts resolved
```

#### Access

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

#### Errors

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

#### See also

- `POST /storefront/discounts/apply`
- `POST /storefront/pricing/calculate-cart`

### 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 cart to price.

```json
{
  "productItems": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2,
      "price": 12
    }
  ],
  "couponCode": "SUMMER20"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The priced cart with all discounts resolved |
| `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/discounts

**List discounts**

`operationId: DiscountController_list`

Lists discounts with optional filtering and paging.

**Mind the `hasCode` inversion.** `hasCode=true` returns code-based discounts and `hasCode=false` returns automatic ones — the parameter is translated to the internal `autoApply` flag, inverted. Omitting it returns both. Any value other than the exact strings `true` and `false` is treated as omitted.

#### Signature

```http
GET /storefront/discounts (status?: string, hasCode?: string, page?: integer, pageSize?: integer) -> A page of discounts
```

#### Access

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

#### Notes

- Unlike the promotions endpoint, this returns full records including targeting and usage — it is an operator read, not a shopper one.

#### Errors

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

#### See also

- `GET /storefront/discounts/promotions/active`

### 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 | string | — | Filter by status, e.g. `active`. |
| `hasCode` | query | "true" \| "false" | — | `true` for code-based discounts, `false` for auto-apply. Omit for both. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of discounts |
| `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/discounts

**Create a discount**

`operationId: DiscountController_create`

Creates a discount or an automatic promotion. Omit `code` and set `autoApply` for one that applies without the shopper typing anything.

Codes are unique: creating one that already exists is rejected.

#### Signature

```http
POST /storefront/discounts (body) -> The created discount
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CODE_EXISTS | Discount code <code> already exists | Another discount in the org already uses that code. | Choose a different code, or update the existing discount instead. |

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

#### See also

- `PUT /storefront/discounts/{identifier}`
- `GET /storefront/discounts`

### 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 discount to create.

```json
{
  "code": "SUMMER20",
  "name": "Summer sale",
  "type": "percent",
  "value": 20,
  "status": "active",
  "minCartValue": 50,
  "usageLimit": 500,
  "endDate": "2026-08-31T23:59:59.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created discount |
| `400` | Discount code <code> already exists — Another discount in the org already uses that 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. |
| `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/discounts/stats

**Get discount statistics**

`operationId: DiscountController_getStats`

Aggregate usage across the org's discounts — how many exist, how many are active, and how much has been redeemed. The operator overview.

#### Signature

```http
GET /storefront/discounts/stats () -> Aggregate discount statistics
```

#### Access

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

#### Notes

- Declared before `GET /storefront/discounts/{identifier}` on the controller, so `stats` resolves as the statistics route and can never be read as a discount named "stats".

#### Errors

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

#### See also

- `GET /storefront/discounts`

### 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 discount 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/discounts/{identifier}

**Get a discount**

`operationId: DiscountController_getByIdentifier`

Fetches one discount by its code or its record id — both resolve through the same lookup.

#### Signature

```http
GET /storefront/discounts/{identifier} (identifier: string) -> The discount
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `PUT /storefront/discounts/{identifier}`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The discount |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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/discounts/{identifier}

**Update a discount**

`operationId: DiscountController_update`

Merges the body into an existing discount. Fields you omit keep their current values; arrays you send replace their counterparts entirely.

#### Signature

```http
PUT /storefront/discounts/{identifier} (identifier: string, body) -> The updated discount
```

#### Access

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

#### Notes

- Changing `value` or `type` affects only future redemptions — orders already discounted keep what they were given.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `PUT /storefront/discounts/{identifier}/activate`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Request body

Fields to change.

```json
{
  "endDate": "2026-09-30T23:59:59.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated discount |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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. |

## DELETE /storefront/discounts/{identifier}

**Delete a discount**

`operationId: DiscountController_delete`

Deletes a discount that has never been redeemed.

**A used discount cannot be deleted** — the redemption history is evidence of what customers were charged, and removing the discount would orphan it. Deactivate instead.

#### Signature

```http
DELETE /storefront/discounts/{identifier} (identifier: string) -> Confirmation of the delete
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `PUT /storefront/discounts/{identifier}/deactivate`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Confirmation of the delete |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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
{
  "success": true
}
```

## PUT /storefront/discounts/{identifier}/activate

**Activate a discount**

`operationId: DiscountController_activate`

Sets the discount status to active so it validates and applies. Start and end dates still gate it — activating a discount whose window has passed does not make it usable.

#### Signature

```http
PUT /storefront/discounts/{identifier}/activate (identifier: string) -> The activated discount
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `PUT /storefront/discounts/{identifier}/deactivate`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The activated discount |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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/discounts/{identifier}/deactivate

**Deactivate a discount**

`operationId: DiscountController_deactivate`

Takes a discount out of service. It stops validating immediately, with `reason: "inactive"`.

This is the reversible way to stop a promotion — prefer it to deleting, which also loses the usage history.

#### Signature

```http
PUT /storefront/discounts/{identifier}/deactivate (identifier: string) -> The deactivated discount
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `PUT /storefront/discounts/{identifier}/activate`
- `DELETE /storefront/discounts/{identifier}`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The deactivated discount |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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/discounts/{identifier}/usage

**Record a discount redemption**

`operationId: DiscountController_recordUsage`

Records that a discount was actually used on an order, incrementing its usage count towards `usageLimit`.

Validation and cart pricing never do this — a code can be validated any number of times without consuming it. Call this once the order is created, or the usage limit will never be reached.

It is **not idempotent**: calling it twice for one order counts two redemptions.

#### Signature

```http
POST /storefront/discounts/{identifier}/usage (identifier: string, body) -> The updated discount, with its incremented usage
```

#### Access

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

#### Notes

- Not idempotent. Guard against double submission, or reverse the surplus with the reverse endpoint.
- Omitting `customerEmail` leaves the first-order-only rule unable to recognise this customer later.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `POST /storefront/discounts/{identifier}/reverse`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Request body

The order the discount was used on.

```json
{
  "orderId": "66f1a2b3c4d5e6f708192a3b",
  "orderNumber": "A7K2M9QX4",
  "discountAmount": 26,
  "customerEmail": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated discount, with its incremented usage |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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/discounts/{identifier}/reverse

**Reverse a discount redemption**

`operationId: DiscountController_reverseUsage`

Undoes a recorded redemption for one order, decrementing the usage count. Use it when an order is cancelled or refunded so the code becomes available again.

#### Signature

```http
POST /storefront/discounts/{identifier}/reverse (identifier: string, body) -> The updated discount
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | DISCOUNT_NOT_FOUND | Discount <identifier> not found | No discount matches that code or id. | List discounts with `GET /storefront/discounts`. Note this is a `400`, not a `404` — do not branch on the status alone. |

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

#### See also

- `POST /storefront/discounts/{identifier}/usage`

### 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. |
| `identifier` | path | string | yes | Discount `code` **or** record id — both resolve. |

### Request body

The order whose redemption is being reversed.

```json
{
  "orderId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated discount |
| `400` | Discount <identifier> not found — No discount matches that code or id. |
| `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. |

