---
title: Pricing
description: Prices, Price Lists, Price Rules, and the Pricing Context — Spree's flexible pricing engine for regional, wholesale, volume, and market-based pricing.
---

## Overview

Spree's pricing system supports both simple single-currency pricing and advanced multi-currency, rule-based pricing through Price Lists. Every [Variant](products.md#variants) can have multiple prices — a base price per currency, plus additional prices from Price Lists that apply conditionally based on rules like geography, customer segment, or quantity.

## Prices

Each variant has one or more `Price` records — one per currency. The API automatically returns the correct price based on the current currency and [Market](markets.md) context. See [monetary amounts](../../api-reference/store-api/monetary-amounts.md) for how money fields (decimal, cents, and display string) are formatted across the API.

| Attribute | Description | Example |
|-----------|-------------|---------|
| `amount` | Current selling price | `"99.90"` |
| `compare_at_amount` | Original/compare price for strikethrough display | `"129.90"` |
| `currency` | ISO 4217 currency code | `USD` |

> **WARNING:** If a product doesn't have a price in the selected currency, it won't appear in Store API responses by default.

### Fetching Prices via API

The Store API returns the resolved price (including Price List rules) for the current currency and market context:


```typescript Store SDK
// Product price is included by default
const product = await client.products.get('spree-tote')
console.log(product.price.amount)              // "15.99" — resolved for current currency
console.log(product.price.compare_at_amount)   // "19.99" — compare-at/strikethrough price (if set)

// Each variant has its own price
const detailed = await client.products.get('spree-tote', {
  expand: ['variants'],
})
detailed.variants?.forEach(variant => {
  console.log(variant.price.amount)            // "15.99"
  console.log(variant.price.compare_at_amount) // "19.99"
})
```

```typescript Admin SDK
const product = await adminClient.products.get('prod_86Rf07xd4z', {
  expand: ['variants'],
})
```

```bash cURL
# Product price is always in the response
curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
  -H 'X-Spree-API-Key: pk_xxx'

# With variants
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=variants' \
  -H 'X-Spree-API-Key: pk_xxx'
```


### Setting Prices via the Admin API

Base prices are set per variant per currency. Use the [Admin API](../../api-reference/admin-api/introduction.md) — via the [Admin SDK](../sdk/admin/quickstart.md) — to set them individually or in bulk (`bulk_upsert` matches on variant + currency):


```typescript Admin SDK
import { createAdminClient } from '@spree/admin-sdk'

const client = createAdminClient({
  baseUrl: 'https://store.example.com',
  secretKey: 'sk_xxx',
})

// Set / change one variant's price in a currency
await client.prices.create({ variant_id: 'variant_xxx', currency: 'USD', amount: '15.99' })

// Upsert many at once
await client.prices.bulkUpsert({
  prices: [
    { variant_id: 'variant_xxx', currency: 'USD', amount: '15.99' },
    { variant_id: 'variant_xxx', currency: 'EUR', amount: '14.99' },
  ],
})
```

```bash CLI
spree api post /prices/bulk_upsert -d '{
  "prices": [
    { "variant_id": "variant_xxx", "currency": "USD", "amount": "15.99" },
    { "variant_id": "variant_xxx", "currency": "EUR", "amount": "14.99" }
  ]
}'
```


## Price Lists

Price Lists allow you to create different pricing strategies based on various conditions. This enables advanced pricing scenarios like:

- **Market-based pricing** — different prices for different [Markets](markets.md) (e.g., North America vs Europe)
- **Regional pricing** — different prices for different geographic zones
- **Wholesale/B2B pricing** — special prices for business customers
- **Volume discounts** — tiered pricing based on quantity purchased
- **Promotional pricing** — time-limited special offers
- **VIP customer pricing** — exclusive prices for specific customers

### How Price Lists Work

When a customer views a product, Spree's pricing resolver determines which price to use:

1. **Price List priority** — Price Lists are ordered by position; higher priority lists are checked first
2. **Status** — only `active` or `scheduled` Price Lists are considered
3. **Date range** — the current time must fall within `starts_at` and `ends_at` (if set)
4. **Price Rules** — all configured rules must match (or any, depending on `match_policy`)

If no applicable Price List is found, the base price is used.

### Price List Attributes

| Attribute | Description |
|-----------|-------------|
| `name` | Human-readable name for the Price List |
| `status` | `draft`, `active`, `scheduled`, or `inactive` |
| `starts_at` | Optional start date when the Price List becomes applicable |
| `ends_at` | Optional end date when the Price List stops being applicable |
| `match_policy` | How rules are evaluated: `all` (every rule must match) or `any` (at least one) |
| `position` | Priority order (lower numbers = higher priority) |

### Creating a Price List via the Admin API

Rules and per-variant price overrides ride on the create/update payload. Each rule is a `{ type, preferences }` draft (see [Price Rules](#price-rules) below):


```typescript Admin SDK
import { createAdminClient } from '@spree/admin-sdk'

const client = createAdminClient({
  baseUrl: 'https://store.example.com',
  secretKey: 'sk_xxx',
})

const priceList = await client.priceLists.create({
  name: 'EU wholesale',
  match_policy: 'all',
  rules: [
    { type: 'market_rule', preferences: { market_ids: ['mkt_xxx'] } },
  ],
  prices: [
    { variant_id: 'variant_xxx', currency: 'EUR', amount: '19.99' },
  ],
})

// Price Lists are draft until activated
await client.priceLists.activate(priceList.id)
```

```bash CLI
spree api post /price_lists -d '{
  "name": "EU wholesale",
  "match_policy": "all",
  "prices": [{ "variant_id": "variant_xxx", "currency": "EUR", "amount": "19.99" }]
}'
spree api patch /price_lists/pl_xxx/activate
```


## Price Rules

Price Rules define conditions that must be met for a Price List to apply. Rules describe the *context* of a purchase — where, through what, how much:

| Rule | Description | Use Case |
|------|-------------|----------|
| **Market Rule** | Matches based on the current [Market](markets.md) | Regional pricing across markets |
| **Channel Rule** | Matches based on the [Channel](channels.md) the customer is buying through | App-only or in-store pricing |
| **Volume Rule** | Matches based on quantity purchased | Bulk discounts, tiered pricing |

*Who* gets a price list is not a rule — it's a [Catalog](catalogs.md): attach the list to a catalog assigned to the customer groups or companies it's for.

> **NOTE:** The **User Rule** and **Customer Group Rule** kinds still work and are not going away, but they're no longer offered when adding a rule — targeting an audience through a catalog is the one way to do it now. Nothing needs migrating; rules you already have keep matching exactly as before.

### Market Rule

Regional pricing. Applies the Price List when the customer is in one of the specified markets.

**Example:** Price a product at $29.99 in North America and €24.99 in Europe, rather than relying on exchange rate conversion.

### Channel Rule

Applies when the customer is buying through a particular sales channel — a discount that exists only in the mobile app, or a price that only applies at a retail till. For new setups, prefer giving the channel a default [Catalog](catalogs.md) that owns the price list — that is where channel-wide commercial terms live going forward.

### Volume Rule

Applies based on quantity purchased. Supports `min_quantity` and `max_quantity` to create tiered pricing:

| Tier | Quantity | Price |
|------|----------|-------|
| Base | 1–9 | $10.00 |
| Bulk Tier 1 | 10–49 | $8.50 |
| Bulk Tier 2 | 50+ | $7.00 |

> **INFO:** Custom Price Rules can be created for specialized pricing logic. See the [Customization Quickstart](../customization/quickstart.md) for details.

## Pricing Context

When resolving prices, Spree considers the full context of the request:

| Context | Source | Description |
|---------|--------|-------------|
| Currency | Market or request header | The currency to price in |
| Market | Customer's country | The [Market](markets.md) for market-based rules |
| Channel | The API credential in use | The [Channel](channels.md) for channel-based rules |
| Customer | Authentication | The signed-in customer, for resolving their [Catalogs](catalogs.md) |
| Quantity | Cart line item | The quantity for volume-based rules |
| Date | Current time | For time-based Price List scheduling |

The Store API automatically builds this context from the [request headers](../../api-reference/store-api/localization.md) (`X-Spree-Currency`, `X-Spree-Country`) and authentication state. You don't need to construct it manually — just make API requests and the correct price is resolved.

> **INFO:** Price resolution runs per request — there is no in-process caching of resolved prices. The Store API instead relies on HTTP/CDN caching, with `Vary` headers keyed on the `X-Spree-Currency` and `X-Spree-Locale` request headers so each currency/locale combination is cached separately at the edge.

## Time-Based Pricing

Price Lists support scheduling through `starts_at` and `ends_at` attributes. Scheduled Price Lists automatically become applicable when the current time falls within their date range — no manual activation needed.

**Example:** A Black Friday sale Price List with `starts_at: 2025-11-28 00:00` and `ends_at: 2025-11-28 23:59` will automatically activate and deactivate.

## Managing Price Lists

Price Lists are managed in the Admin Panel under **Products → Price Lists**, or via the Admin API.

Each Price List contains prices for specific variants and currencies. Products can be added to a Price List, and individual variant prices set within it.

## Price History (EU Omnibus Directive)

Spree automatically records price changes for EU Omnibus Directive compliance. When a product goes on sale, EU regulations require displaying the lowest price in the preceding 30 days alongside the discounted price.

### How It Works

Every time a base price amount changes, a `PriceHistory` record is created automatically. Price list prices are not tracked — only the base price visible to all customers.

### Fetching Prior Price via API

The prior price is available as an expandable field on product and variant endpoints:


```typescript Store SDK
const product = await client.products.get('spree-tote', {
  expand: ['prior_price'],
})

if (product.prior_price) {
  console.log(product.prior_price.amount)         // "9.99"
  console.log(product.prior_price.display_amount)  // "$9.99"
  console.log(product.prior_price.currency)        // "USD"
  console.log(product.prior_price.recorded_at)     // "2026-03-10T..."
}
```

```typescript Admin SDK
const product = await adminClient.products.get('prod_86Rf07xd4z', {
  expand: ['prior_price'],
})
```

```bash cURL
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=prior_price' \
  -H 'X-Spree-API-Key: pk_xxx'
```


The response includes a `prior_price` object when expanded:

```json Response
{
  "id": "prod_xxx",
  "name": "Spree Tote",
  "price": { "amount": "15.99", "currency": "USD", "display_amount": "$15.99" },
  "prior_price": {
    "amount": "9.99",
    "amount_in_cents": 999,
    "currency": "USD",
    "display_amount": "$9.99",
    "recorded_at": "2026-03-10T14:30:00Z"
  }
}
```

### Configuration

Price history tracking is enabled by default. To disable it (e.g., for non-EU stores):


```typescript Admin SDK
await client.store.update({ preferred_track_price_history: false })
```

```bash cURL
curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
  -H 'X-Spree-API-Key: sk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{ "preferred_track_price_history": false }'
```


Retention defaults to 30 days, set per store with the `price_history_retention_days` preference. A Rake task prunes anything older:


```bash Spree CLI (Docker)
spree rake spree:price_history:prune
```

```bash Without Spree CLI
bundle exec rake spree:price_history:prune
```


### Seeding Existing Prices

After enabling price history on an existing store, seed the current prices as a baseline:


```bash Spree CLI (Docker)
spree rake spree:price_history:seed
```

```bash Without Spree CLI
bundle exec rake spree:price_history:seed
```


## Related Documentation

- [Products](products.md) — Products, Variants, and base prices
- [Store SDK: Products](../sdk/store/products.md) — Fetching product and variant prices with the Store SDK
- [Markets](markets.md) — Geographic regions with currency and locale
- [Taxes](taxes.md) — Tax categories, tax rates, and zones
- [Promotions](promotions.md) — Discount-based pricing via promotion rules
