docs/developer/core-concepts/pricing.mdx
Spree's pricing system supports both simple single-currency pricing and advanced multi-currency, rule-based pricing through Price Lists. Every Variant 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.
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 context.
| 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 |
The Store API returns the resolved price (including Price List rules) for the current currency and market context:
<CodeGroup>// Product price is included by default
const product = await client.products.get('spree-tote')
console.log(product.price) // "15.99" — resolved for current currency
console.log(product.original_price) // "19.99" — compare-at 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) // "15.99"
console.log(variant.original_price) // "19.99"
})
# Product price is always in the response
curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
-H 'Authorization: Bearer pk_xxx'
# With variants
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=variants' \
-H 'Authorization: Bearer pk_xxx'
Price Lists allow you to create different pricing strategies based on various conditions. This enables advanced pricing scenarios like:
When a customer views a product, Spree's pricing resolver determines which price to use:
active or scheduled Price Lists are consideredstarts_at and ends_at (if set)match_policy)If no applicable Price List is found, the base price is used.
| 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) |
Price Rules define conditions that must be met for a Price List to apply. Spree includes five built-in rule types:
| Rule | Description | Use Case |
|---|---|---|
| Market Rule | Matches based on the current Market | Regional pricing across markets |
| Zone Rule | Matches based on the customer's geographic zone | Country or state-level pricing |
| User Rule | Matches specific customer accounts | VIP customers, wholesale accounts |
| Customer Group Rule | Matches members of customer groups | Loyalty tiers, membership pricing |
| Volume Rule | Matches based on quantity purchased | Bulk discounts, tiered pricing |
The recommended approach for regional pricing when using Markets. 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.
Applies based on the customer's geographic zone. Useful for regional or country-specific pricing when not using Markets.
Limits the Price List to specific customer accounts. Useful for VIP customers, employee pricing, or wholesale accounts.
Applies to members of specific customer groups. Useful for wholesale tiers, loyalty programs, or membership-based pricing.
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 |
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 for market-based rules |
| Zone | Customer's address | The geographic zone for zone-based rules |
| Customer | JWT authentication | The logged-in customer for user-based rules |
| 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 (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.
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.
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.
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.
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.
The prior price is available as an expandable field on product and variant endpoints:
<CodeGroup>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..."
}
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:
{
"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"
}
}
Price history tracking is enabled by default. To disable it (e.g., for non-EU stores):
# config/initializers/spree.rb
Spree.config do |config|
config.track_price_history = false
config.price_history_retention_days = 30 # default
end
Price history retention defaults to 30 days and can be configured globally. A Rake task is provided for cleanup:
bundle exec rake spree:price_history:prune
After enabling price history on an existing store, seed the current prices as a baseline:
bundle exec rake spree:price_history:seed