# Storefront · Pricing

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

**Calculate a product price**

`operationId: PricingController_calculatePrice`

Resolves what one product costs for the calling customer at a given quantity.

Pricing runs as an ordered pipeline, and the first stage that produces a price stops the search for a base:

1. The product's base price, or the absolute price of a matched variation.
2. Product-level **tiered** pricing, when the quantity qualifies.
3. Product-level **group** pricing, when the customer is in a qualifying group.
4. **Price lists**, used only as a fallback when none of the above applied.
5. **Group benefits** — a further discount from the customer's group.
6. **Auto-apply discounts**.

Per-option surcharges are deliberately added *after* the pipeline, not folded into the base: tier and price-list prices are themselves base prices, so adding the surcharge first would let a tier overwrite it.

The customer is taken from the authenticated caller, not the body — an anonymous request gets anonymous pricing. Use `preview-as-customer` to price as someone else.

#### Signature

```http
POST /storefront/pricing/calculate (body) -> The resolved price, with everything that contributed
```

#### Access

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

#### Notes

- The price depends on who is calling. Do not cache a result across customers.
- `quantity` changes the *unit* price when a tier applies — it is not just a multiplier.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PRODUCT_NOT_FOUND | The requested resource was not found | The `sku` does not resolve to a product in the org. | Check the SKU with `GET /storefront/products`. The server knows which one was missing but does not tell you: the underlying error is a plain `Error`, and the global filter rewrites its message to the generic text for anything that is not an `HttpException`. |

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

#### See also

- `POST /storefront/pricing/calculate-cart`
- `POST /storefront/pricing/preview-as-customer`

### 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 product and quantity to price.

```json
{
  "sku": "DRK-COLA-330"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The resolved price, with everything that contributed |
| `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` | The requested resource was not found — The `sku` does not resolve to a product in the 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
{
  "originalPrice": 12,
  "finalPrice": 9.6,
  "unitPrice": 9.6,
  "totalPrice": 115.2,
  "discount": 2.4,
  "discountPercent": 20,
  "quantity": 12,
  "currency": "USD",
  "appliedDiscounts": [
    {
      "name": "bulk-12",
      "amount": 2.4
    }
  ]
}
```

## POST /storefront/pricing/calculate-cart

**Calculate cart prices**

`operationId: PricingController_calculateCartPrices`

Prices a whole cart in one pass, including coupon application and any automatic discounts.

This is handled by the discount service rather than the pricing service, deliberately: cart-level discounting is a single source of truth, so a coupon and an auto-apply rule cannot both claim the same basket through different code paths.

Products and rentals are priced together but on their own terms — pass each in its own array.

**Render `total` as given.** It already includes tax and shipping. A client that computes `subtotal + shipping - discount` drops tax entirely — and agrees with the server on tax-free destinations, which is exactly why that bug survives review.

#### Signature

```http
POST /storefront/pricing/calculate-cart (body) -> The priced cart with totals and applied discounts
```

#### Access

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

#### Notes

- An invalid coupon does not fail the request — inspect the response to find out whether it applied.
- A product's `data.calculatedPrice` is an OBJECT — `{ finalPrice, originalPrice, discount, discountPercent, appliedDiscounts }` — not a number. Read `.finalPrice`; rendering the object yields `[object Object]`.
- Checkbox-style options contribute the SUM of every checked option's price. Quantity multiplies the line and is not a delta.

#### Errors

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

#### See also

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

### 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
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The priced cart with totals and applied 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. |

## GET /storefront/pricing/product/{sku}

**Get pricing variations for a product**

`operationId: PricingController_getProductPricingInfo`

Returns every price this product can take for the calling customer — the tiers, the group prices and the price-list entries that could apply — rather than resolving a single one.

Use it to render a "buy 12 for $9 each" table, where the customer needs to see the ladder rather than just the price at their current quantity.

#### Signature

```http
GET /storefront/pricing/product/{sku} (sku: string) -> The pricing variations available to this customer
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PRODUCT_NOT_FOUND | The requested resource was not found | The `sku` does not resolve to a product in the org. | Check the SKU with `GET /storefront/products`. The server knows which one was missing but does not tell you: the underlying error is a plain `Error`, and the global filter rewrites its message to the generic text for anything that is not an `HttpException`. |

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

#### See also

- `POST /storefront/pricing/calculate`

### 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. |
| `sku` | path | string | yes | Product SKU. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The pricing variations available to this customer |
| `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` | The requested resource was not found — The `sku` does not resolve to a product in the 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/pricing/customer-price-lists

**Get the calling customer's price lists**

`operationId: PricingController_getCustomerPriceLists`

Returns the active price lists that target the signed-in customer, ordered by descending priority, together with the groups they belong to.

An anonymous caller gets empty arrays rather than an error, so a storefront can call this before sign-in without special-casing.

#### Signature

```http
GET /storefront/pricing/customer-price-lists () -> The customer's applicable price lists and groups
```

#### Access

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

#### Notes

- Anonymous callers receive `{ priceLists: [], customerGroups: [] }` with a `200`.
- Only lists with `status: "active"` are considered — a draft list never appears here.

#### Errors

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

#### See also

- `GET /storefront/pricing/price-lists`

### 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 applicable price lists and groups |
| `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
{
  "priceLists": [],
  "customerGroups": []
}
```

## GET /storefront/pricing/price-lists

**List price lists**

`operationId: PricingController_listPriceLists`

Returns every price list in the org, regardless of status, with a total. Unpaged.

#### Signature

```http
GET /storefront/pricing/price-lists () -> All price lists
```

#### Access

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

#### Notes

- Includes inactive and draft lists — filter on `data.status` if you only want the live ones.

#### Errors

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

#### See also

- `POST /storefront/pricing/price-lists`

### 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` | All price lists |
| `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/pricing/price-lists

**Create a price list**

`operationId: PricingController_createPriceList`

Creates a price list from the body as given.

**Nothing is validated and nothing is de-duplicated.** There is no uniqueness check on `name`, so creating a list with an existing name succeeds and leaves two lists sharing it — after which `GET`, `PUT` and `DELETE` by that name all act on whichever the lookup returns first. Check the name is free before creating.

The record is authored as `system` rather than the calling user.

#### Signature

```http
POST /storefront/pricing/price-lists (body) -> The created price list
```

#### Access

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

#### Notes

- Duplicate names are accepted. The name is the identifier used by every other endpoint here, so a duplicate makes those endpoints ambiguous.
- Only `status: "active"` lists take part in pricing.
- Records are authored as `system`, so the audit trail does not name the operator who created it.

#### Errors

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

#### See also

- `PUT /storefront/pricing/price-lists/{name}`
- `GET /storefront/pricing/price-lists`

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

```json
{
  "name": "wholesale",
  "title": "Wholesale pricing",
  "status": "active",
  "priority": 10,
  "currency": "USD",
  "customerGroups": [
    "wholesale"
  ],
  "prices": [
    {
      "sku": "DRK-COLA-330",
      "price": 9,
      "minQuantity": 12
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created price list |
| `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/pricing/price-lists/{name}

**Get a price list**

`operationId: PricingController_getPriceList`

Fetches one price list by name.

A name that does not exist returns **`null` with a `200`**, not a `404` — this read does not throw. Check for a null body.

#### Signature

```http
GET /storefront/pricing/price-lists/{name} (name: string) -> The price list, or `null` when no list has that name
```

#### Access

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

#### Notes

- Unlike update and delete, a missing list is `null` + `200` here rather than a `404`.

#### Errors

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

#### See also

- `PUT /storefront/pricing/price-lists/{name}`

### 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. |
| `name` | path | string | yes | Price list `name`, not its `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The price list, or `null` when no list has that name |
| `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/pricing/price-lists/{name}

**Update a price list**

`operationId: PricingController_updatePriceList`

Merges the body into the existing list — fields you omit keep their current values.

**`name` cannot be changed.** It is re-applied from the path after the merge, so sending a different `name` in the body is silently ignored rather than rejected. To rename a list, create a new one and delete the old.

Note the merge is shallow: sending `prices` replaces the whole array rather than merging entries into it.

#### Signature

```http
PUT /storefront/pricing/price-lists/{name} (name: string, body) -> The updated price list
```

#### Access

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

#### Notes

- A `name` in the body is discarded — the path wins.
- `prices` and other arrays are replaced, not merged.
- Updates are authored as `system`, not the calling user.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PRICE_LIST_NOT_FOUND | The requested resource was not found | No price list in the org has that name. | List them with `GET /storefront/pricing/price-lists`. The server knows which one was missing but does not tell you: the underlying error is a plain `Error`, and the global filter rewrites its message to the generic text for anything that is not an `HttpException`. |

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

#### See also

- `GET /storefront/pricing/price-lists/{name}`
- `DELETE /storefront/pricing/price-lists/{name}`

### 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. |
| `name` | path | string | yes | Price list `name`, not its `sk`. |

### Request body

Fields to change. Omitted fields are left as they are.

```json
{
  "status": "inactive"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated price list |
| `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` | The requested resource was not found — No price list in the org has that name. |
| `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/pricing/price-lists/{name}

**Delete a price list**

`operationId: PricingController_deletePriceList`

Permanently deletes a price list. This is a hard delete, not a soft one — the record is removed rather than flagged, so there is nothing to restore.

Customers currently priced by this list fall back to whatever the pipeline resolves next: another targeting list, or the product's own price. Deactivate the list first (`status: "inactive"`) if you want to see that effect before it becomes irreversible.

#### Signature

```http
DELETE /storefront/pricing/price-lists/{name} (name: string) -> Confirmation of the delete
```

#### Access

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

#### Notes

- Hard delete — the record is gone, not archived.
- Prefer setting `status: "inactive"` to take a list out of service reversibly.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PRICE_LIST_NOT_FOUND | The requested resource was not found | No price list in the org has that name. | List them with `GET /storefront/pricing/price-lists`. The server knows which one was missing but does not tell you: the underlying error is a plain `Error`, and the global filter rewrites its message to the generic text for anything that is not an `HttpException`. |

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

#### See also

- `PUT /storefront/pricing/price-lists/{name}`

### 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. |
| `name` | path | string | yes | Price list `name`, not its `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Confirmation of the delete |
| `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` | The requested resource was not found — No price list in the org has that name. |
| `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
}
```

## POST /storefront/pricing/preview-as-customer

**Preview a price as another customer**

`operationId: PricingController_previewAsCustomer`

Prices a product as a specified customer rather than the caller — the operator tool for answering "what does this cost for that wholesale account?" without signing in as them.

Omitting `customerId` prices it anonymously, which is the useful way to see the public price.

#### Signature

```http
POST /storefront/pricing/preview-as-customer (body) -> The price as that customer would see it
```

#### Access

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

#### Notes

- This exposes another customer's negotiated pricing — treat it as an operator endpoint and do not proxy it to a storefront.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PRODUCT_NOT_FOUND | The requested resource was not found | The `sku` does not resolve to a product in the org. | Check the SKU with `GET /storefront/products`. The server knows which one was missing but does not tell you: the underlying error is a plain `Error`, and the global filter rewrites its message to the generic text for anything that is not an `HttpException`. |

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

#### See also

- `POST /storefront/pricing/calculate`

### 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

What to price, and as whom.

```json
{
  "sku": "DRK-COLA-330",
  "quantity": 12,
  "customerId": "cus_4821"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The price as that customer would see it |
| `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` | The requested resource was not found — The `sku` does not resolve to a product in the 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. |

