pricing
npx skills add https://github.com/crystallizeapi/ai --skill pricingCrystallize Pricing Skill
Design, recommend, and implement pricing strategies in Crystallize. This skill covers the full pricing hierarchy — from global price variants through localized price lists to cart-level promotions — and helps you make sound architectural decisions about how to structure pricing for any commerce use case.
The Pricing Hierarchy
Crystallize resolves prices through a layered hierarchy. Each layer can override or adjust the one above it:
┌─────────────────────────────────────────────────┐│ 1. Price Variants (base layer) ││ Global price types applied to all products ││ e.g. Retail, Sales, B2B, Members │├─────────────────────────────────────────────────┤│ 2. Price Lists (override layer) ││ Market/customer-specific adjustments ││ e.g. "EU Retail EUR", "US B2B USD" │├─────────────────────────────────────────────────┤│ 3. Promotions (cart layer) ││ Checkout-time discounts and campaigns ││ e.g. "20% off", "Buy 3 pay for 2" │└─────────────────────────────────────────────────┘Resolution order: When a customer checks out, the system evaluates:
- Which price variant applies (based on context)
- Whether a price list overrides that variant (based on market, customer group, time period)
- Whether any promotions apply (based on cart contents, coupon codes, triggers)
Quick Start Decision Tree
START: You need to set up pricing.
Q1: Do you sell in a single currency with one price per product? → Yes → Create 1 price variant (e.g. "default" in your currency) → No → Continue
Q2: Do you need different price *types* (retail vs wholesale, regular vs sale)? → Yes → Create a price variant per type (see Price Variant Patterns below)
Q3: Do you sell in multiple currencies or regions? → Yes → Create Markets + Price Lists (see Markets & Price Lists below)
Q4: Do you need temporary discounts, coupon codes, or "buy X get Y" deals? → Yes → Set up Promotions (see Promotions below)
Q5: Do you have customer-specific pricing (B2B agreements, VIP tiers)? → Yes → Use Price Lists targeted to customer groupsPrice Variants
Price variants are global definitions — they define the types of prices available across your entire catalogue. Every product variant can then have a value for each price variant.
Key Concepts
- Name: Human-readable (e.g. “US Dollar Retail”)
- Identifier: API-safe, lowercase (e.g.
usd-retail) - Currency: ISO 4217 code (e.g.
USD,EUR,NOK) - A single price variant = one currency. For multi-currency, create separate variants.
The Default Price Variant
Every Crystallize tenant comes with a default price variant. It must always exist — it is the system’s fallback and cannot be deleted.
Best practice: Rename it to match your primary use case.
| Business Type | Rename default to | Currency | Rationale |
|---|---|---|---|
| B2C single market | retail | Your main currency | The standard consumer price |
| B2C multi-market | retail (main market currency) | Main market currency | The primary market’s retail price |
| B2B single market | b2b or wholesale | Your main currency | Your main B2B list price |
| B2B + B2C | retail (main market) | Main market currency | Retail is the most common base |
The default variant serves as the fallback everywhere — if no price list override or more specific variant applies, this is the price the system resolves to. So it should represent your most common, primary price type for your main market.
Naming Conventions
Use clear, consistent names that encode purpose and currency:
| Pattern | Example Identifier | Example Name | Currency |
|---|---|---|---|
| Single market | default | Default | USD |
| Currency-based | usd, eur, nok | US Dollar, Euro, Norwegian Krone | respective |
| Purpose-based | retail, sales, b2b | Retail, Sales, B2B | same |
| Combined | usd-retail, eur-b2b | USD Retail, EUR B2B | respective |
Recommended Patterns by Business Type
Simple B2C (single market)
Price Variants: └── default (USD) — The only price, used everywhereWhen: Single-country store, one currency, no B2B. Start here and add complexity later.
B2C with Sales Pricing (single market)
Price Variants: ├── retail (USD) — The standard price / compare-at price ("was" price) └── sales (USD) — The marked-down selling price ("now" price)When: You show was/now or strikethrough pricing. Retail is the reference price — the “compare at” price displayed crossed out. Sales is the actual selling price. When no sale is active, the storefront falls back to the Retail price (Sales is left empty or equal to Retail).
Key rule: Retail is always set. Sales is only set when the product is on sale. The storefront checks: if Sales exists and is lower than Retail → show strikethrough. Otherwise → show Retail as the current price.
B2C Per-Market (the most typical B2C pattern)
Price Variants: ├── retail (USD) — US retail (the default variant, renamed) ├── sales (USD) — US sales / marked-down price ├── retail-eur (EUR) — EU retail ├── sales-eur (EUR) — EU sales ├── retail-gbp (GBP) — UK retail └── sales-gbp (GBP) — UK salesWhen: Multi-market B2C with was/now pricing per market. Each market gets a Retail + Sales pair. The default variant is renamed to retail for the primary market. Secondary markets use retail-{currency} / sales-{currency} suffixes.
Resolution per market:
- Primary market (US): Check
sales→ fall back toretail - EU market: Check
sales-eur→ fall back toretail-eur - UK market: Check
sales-gbp→ fall back toretail-gbp
Price Lists can further override any of these per market.
B2C Multi-Currency (no sales pricing)
Price Variants: ├── usd (USD) — US market price ├── eur (EUR) — European market price └── gbp (GBP) — UK market priceWhen: You sell internationally with manually set prices per currency but don’t show was/now pricing. Simpler than the retail/sales pattern.
B2C + B2B (single currency)
Price Variants: ├── retail (USD) — Consumer price / compare-at reference ├── sales (USD) — Consumer sale price (when on sale) └── b2b (USD) — Wholesale/business priceWhen: You serve both consumers and business customers in the same currency. B2B customers typically don’t see sales pricing — they have fixed contract rates.
B2C + B2B Multi-Currency
Price Variants: ├── retail (USD) — US consumer price (default, renamed) ├── sales (USD) — US consumer sale price ├── b2b (USD) — US wholesale price ├── retail-eur (EUR) — EU consumer price ├── sales-eur (EUR) — EU consumer sale price ├── b2b-eur (EUR) — EU wholesale price └── ...per additional marketWhen: International B2B+B2C. Each market gets a retail/sales pair for consumers plus a B2B variant. The most complex but most flexible setup.
Membership / Loyalty Tiers
Price Variants: ├── retail (USD) — Standard consumer price ├── members (USD) — Loyalty program price └── vip (USD) — Top-tier customer priceWhen: You have tiered customer programmes. Often combined with Price Lists for more granular control.
How Many Price Variants?
Guidelines:
- Start minimal — You can always add more. Begin with 1–2 and expand when the business requires it.
- Avoid explosion — 10+ variants is a warning sign. Use Price Lists for market-specific overrides instead.
- One per purpose × currency — If you have 3 currencies and 2 purposes (retail + B2B), that’s 6 variants.
- Don’t duplicate what Price Lists do — Price variants are for structurally different price types. Regional adjustments belong in Price Lists.
| Business Complexity | Typical Count | Variants |
|---|---|---|
| Simple single-market | 1 | default |
| B2C with sales pricing | 2 | retail + sales |
| Multi-currency B2C | 2–5 | One per currency |
| B2C + B2B single currency | 2–3 | retail + sales + b2b |
| Enterprise multi-market | 4–8 | Purpose × currency |
Common Mistakes
- Creating a variant per country — Use Price Lists for regional adjustments instead
- Leaving “default” named as “default” — Always rename it to reflect its purpose (e.g.
retailfor B2C,b2bfor B2B). The name “default” tells nobody what the price represents. - Confusing retail and sales — Retail is the reference/compare-at price (always set). Sales is the marked-down price (only set during a sale). The storefront falls back to Retail when Sales is empty.
- Using promotions when you need persistent sales pricing — Promotions are for cart-level, time-limited campaigns. If you always show a was/now price on the product page, use a
salesprice variant instead. - Inconsistent naming — Pick a convention (
{purpose}-{currency}or{purpose}) and stick with it across all variants - No currency suffix for secondary markets — When multi-currency, primary market variants can be just
retail/sales, but secondary markets should include a currency suffix:retail-eur,sales-eur - Too many variants — 10+ variants is a red flag. Use Price Lists for customer-specific or regional fine-tuning instead of creating more variants
Price Lists
Price lists override or adjust prices for specific contexts. They sit between variants and promotions in the hierarchy.
When to Use Price Lists (not Price Variants)
| Scenario | Use Price Variant? | Use Price List? |
|---|---|---|
| USD vs EUR base prices | ✅ Yes | No |
| B2B vs Retail price type | ✅ Yes | No |
| ”Norway gets 10% less than global EUR” | No | ✅ Yes |
| ”VIP customers get special prices” | No | ✅ Yes |
| ”Black Friday EU pricing” | No | ✅ Yes |
| ”Wholesale customer X negotiated rate” | No | ✅ Yes |
Price List Configuration
Each price list can:
- Target specific price variants (e.g. only adjust B2B prices)
- Apply to all or specific products (by drag-and-drop or bulk selection)
- Adjust by: percentage, relative value, or absolute price
- Have a time period: start/end dates for seasonal campaigns
- Be scoped to: a market, customer group, or individual customer
Recommended Price List Patterns
Regional Adjustments
Price Lists: ├── "EU Retail" — Applies to: eur-retail variant, Market: EU ├── "US Retail" — Applies to: usd-retail variant, Market: US └── "Nordic B2B" — Applies to: eur-b2b variant, Market: NordicsCustomer-Specific B2B
Price Lists: ├── "Wholesale Tier 1" — 15% off B2B variant, Customer Group: Tier 1 ├── "Wholesale Tier 2" — 25% off B2B variant, Customer Group: Tier 2 └── "Acme Corp Agreement" — Fixed prices, Customer: Acme CorpSeasonal Campaigns
Price Lists: ├── "Summer Sale EU" — 20% off retail, Market: EU, Period: Jun–Aug └── "Black Friday Global" — Specific prices, Period: Nov 25–28Markets
Markets define selling contexts. They group together the rules for who gets which prices, promotions, and configurations.
What Markets Represent
A market can be:
- A country (Norway, Sweden, Germany)
- A region (EU, Nordics, North America)
- A segment (Norway B2B, Norway Retail)
- A channel (Online, In-Store, Marketplace)
Market Setup Recommendations
Simple: One Market
Markets: └── "default" — Your only selling contextMulti-Region
Markets: ├── "us" — United States ├── "eu" — European Union ├── "uk" — United Kingdom └── "row" — Rest of World (fallback)B2B + B2C per Region
Markets: ├── "eu-retail" — EU consumers ├── "eu-b2b" — EU businesses ├── "us-retail" — US consumers └── "us-b2b" — US businessesHow Markets Connect to Pricing
Market ──→ Price Lists (which prices apply) ──→ Promotions (which campaigns apply) ──→ Checkout context (resolved at purchase time)Markets are set in the checkout context — at cart/checkout time, the active market determines which price lists and promotions are evaluated.
Promotions
Promotions apply at the cart level — they only become visible during checkout, not on product pages. This keeps your base pricing clean while enabling flexible campaigns.
Promotion Mechanisms
| Mechanism | Description | Example |
|---|---|---|
| Percentage | % off the price | ”20% off all shoes” |
| Fixed | Flat amount off | ”$10 off orders over $50” |
| X for Y | Buy X, pay for Y | ”Buy 3, pay for 2” |
| Cart | Reduce cart total | ”$20 off the entire cart” |
Promotion Components
- Mechanism — How the discount is calculated (percentage, fixed, X-for-Y, cart)
- Period (optional) — When the promotion is active (start/end dates, can recur)
- Trigger (optional) — What activates it (e.g. “3 items of product X in cart”)
- Target (optional) — Which products are affected (if empty, applies to all)
- Limitations — Cumulative rules, max usage, per-customer limits
When to Use Promotions vs Price Lists
| Scenario | Promotions | Price Lists |
|---|---|---|
| Time-limited discounts | ✅ | Possible but less ideal |
| Coupon codes | ✅ | ❌ |
| “Buy X get Y” deals | ✅ | ❌ |
| Permanent regional pricing | ❌ | ✅ |
| Customer-specific agreements | ❌ | ✅ |
| Free shipping thresholds | ✅ | ❌ |
Consultation Approach
When advising on pricing setup, ask these discovery questions:
Discovery Questions
-
Business Model
- “Is this B2C, B2B, or both?”
- “Do you have tiered pricing for different customer groups?”
-
Geographic Scope
- “Which markets/countries do you sell to?”
- “Do you need separate currencies, or do you use a single currency?”
- “Are prices manually set per region, or derived from a base price?”
-
Sales Strategy
- “Do you run sales or show ‘was/now’ pricing?”
- “Do you use coupon codes or promotional campaigns?”
- “Do you have seasonal pricing patterns?”
-
Customer Relationships
- “Do you have wholesale/B2B customers with negotiated rates?”
- “Do you have a loyalty or membership programme?”
- “Do individual customers have unique pricing agreements?”
-
Complexity Assessment
- “How many products are in your catalogue?”
- “How often do prices change?”
- “Who manages pricing? (technical team vs merchandisers)“
Response Framework
After discovery, structure your recommendation as:
- Price Variants — The base layer: how many, naming, currencies
- Markets — If multi-region: how to segment
- Price Lists — Regional/customer overrides (if needed)
- Promotions — Campaign strategy (if needed)
- Migration path — Start simple, grow into complexity
Always recommend starting with the minimum viable setup and adding layers as the business requires them.
Tax & Pricing
Prices in Crystallize are stored as raw numbers — the system does not enforce whether they are tax-inclusive or tax-exclusive. This is a business decision:
- B2C (most of Europe, Australia): Prices typically include VAT. Store gross prices in your variants.
- B2B / US B2C: Prices are typically tax-exclusive. Store net prices and calculate tax at checkout.
- Mixed: Use price variants or price lists to maintain both. For example, EU retail variants hold gross prices while US retail variants hold net prices.
Tax calculation itself happens at the checkout/order layer — configure tax rates in your checkout flow or use an external tax service. The pricing hierarchy (variants → price lists → promotions) resolves the price; tax is applied on top of (or extracted from) that resolved price.
Migration Paths
Start simple and add layers as your business requires. Here are common upgrade paths:
Single variant → Sales pricing
Before: default (USD)After: retail (USD) + sales (USD)Rename default to retail. Create a new sales variant in the same currency. Only set sales on products that are currently on sale.
Single currency → Multi-currency
Before: retail (USD) + sales (USD)After: retail (USD) + sales (USD) + retail-eur (EUR) + sales-eur (EUR)Keep your primary market variants as-is. Add new variant pairs for each additional currency. Create Markets for each region and Price Lists if you need regional adjustments beyond the base variant prices.
B2C only → B2C + B2B
Before: retail (USD) + sales (USD)After: retail (USD) + sales (USD) + b2b (USD)Add a b2b variant. Create a Market for B2B customers (e.g. us-b2b). Use Price Lists for customer-specific or tiered B2B pricing.
Static pricing → Campaigns
Before: Price variants onlyAfter: Price variants + PromotionsKeep your variant structure unchanged. Add Promotions for time-limited, cart-level discounts (coupons, buy-X-get-Y, seasonal sales). If you need persistent regional adjustments, add Price Lists instead of (or alongside) Promotions.
Further Reading
- Price Variants Reference — Detailed technical reference for variant setup
- Price Lists & Markets Reference — Price list configuration and market architecture
- Promotions Reference — Promotion types, mechanisms, and limitations
Reference Details
Price Lists & Markets Reference
Price lists and markets work together to localize and personalize pricing. Markets define where and who; price lists define what price they get.
Markets
What Markets Are
A market is a named selling context. It groups together the pricing, promotion, and configuration rules that apply when a customer checks out.
Markets are set at checkout time via the cart context — they are not assigned to products directly.
Creating Markets
In the Admin UI:
- Go to Settings → Markets
- Click Add market +
- Enter a name (e.g. “Norway B2B”) and identifier (e.g.
norway-b2b) - Click Create market
The identifier is used in the API and checkout context. Choose it carefully — it should be lowercase, hyphenated, and descriptive.
Via PIM API
mutation CreateMarket { market { create( input: { tenantId: "your-tenant-id" identifier: "eu-retail" name: "EU Retail" customerIdentifiers: [] type: B2C } ) { identifier name } }}Market Architecture Patterns
By Country
Markets: ├── norway — "Norway" ├── sweden — "Sweden" ├── germany — "Germany" └── us — "United States"By Region
Markets: ├── nordics — "Nordics" (NO, SE, DK, FI) ├── eu — "European Union" ├── uk — "United Kingdom" └── us — "United States"By Segment × Region
Markets: ├── eu-retail — "EU Retail" ├── eu-b2b — "EU B2B" ├── us-retail — "US Retail" └── us-b2b — "US B2B"By Channel
Markets: ├── online — "Online Store" ├── in-store — "Physical Stores" └── marketplace — "Marketplace"How Markets Connect to Checkout
At checkout time, the storefront sets the market in the cart context:
mutation HydrateCart { cart { hydrate(context: { markets: ["eu-retail"] }, input: { items: [{ sku: "TSHIRT-RED-L", quantity: 1 }] }) { cart { items { variant { sku name } price { gross net currency } } total { gross net currency } } } }}The market selection determines:
- Which price lists are evaluated
- Which promotions apply
- Which currency is resolved
Price Lists
What Price Lists Do
Price lists override or adjust the base price (from price variants) for specific contexts. They answer: “Given this market / customer group / time period, what price should this customer see?”
Creating Price Lists
In the Admin UI:
- Go to Special Prices → Price Lists
- Click Add new
- Configure:
- Name and identifier — e.g. “EU Summer Sale”,
eu-summer-sale - Products — All products, or specific ones (drag-and-drop / bulk select)
- Price variants — Which variant(s) this list adjusts
- Adjustment type — Percentage, relative value, or absolute price
- Period (optional) — Start and end dates
- Target — Market, customer group, or individual customer
- Name and identifier — e.g. “EU Summer Sale”,
Via PIM API
mutation CreatePriceList { priceList { create( input: { tenantId: "your-tenant-id" identifier: "eu-summer-sale" name: "EU Summer Sale" modifierType: PERCENTAGE priceVariants: ["retail"] selectedProductVariants: { type: ALL } targetAudience: { marketIdentifiers: ["eu-retail"] } startDate: "2025-06-01T00:00:00Z" endDate: "2025-08-31T23:59:59Z" } ) { identifier name } }}Adjustment Types
| Type | Description | Example |
|---|---|---|
| Percentage | Adjust up or down by % | -10% = 10% discount |
| Relative | Add or subtract a fixed amount | -5 = $5 off |
| Absolute | Set a specific price | 25.00 = exactly $25 |
Price List Patterns
Regional Price Adjustments
Different prices for different regions, all based on the same variants:
Price Variant: "retail" (EUR) — Base price: €100
Price Lists: ├── "Nordic Retail" → Market: nordics → Adjust: -5% → Final: €95 ├── "Southern EU Retail" → Market: southern-eu → Adjust: +10% → Final: €110 └── "UK Retail" → Market: uk → Adjust: absolute £89 → Final: £89Customer Group Tiering
Price Variant: "b2b" (EUR) — Base price: €80
Price Lists: ├── "Wholesale Tier 1" → Group: tier-1 → Adjust: -10% → Final: €72 ├── "Wholesale Tier 2" → Group: tier-2 → Adjust: -20% → Final: €64 └── "Strategic Partner" → Group: strategic → Adjust: -30% → Final: €56Time-Based Campaigns
Price Lists: ├── "Summer Sale 2025" │ Period: Jun 1 – Aug 31 │ Adjust: -25% on retail variant │ Market: all │ └── "Black Friday EU" Period: Nov 25 – Nov 28 Adjust: absolute prices (manually set per product) Market: euIndividual Customer Agreements (B2B)
For B2B customers with negotiated rates:
Price Lists: └── "Acme Corp Agreement" Customer: Acme Corp (organization) Products: Specific items Adjust: absolute prices (per agreement) Period: Jan 1 – Dec 31 (annual renewal)Resolution Priority
When multiple price lists match a context, Crystallize evaluates them in this order:
- Most specific target wins — Individual customer > customer group > market > global
- Active period — Only lists within their defined period are evaluated
- Product scope — Product-specific lists override “all products” lists
Best Practices
- Name descriptively — Include the target context in the name: “EU B2B Q1 2025” not “Price List 3”
- Limit overlap — Avoid having many price lists targeting the same market+variant combination
- Use periods — Even for “permanent” adjustments, set a far-future end date so they can be reviewed
- Bulk select products — Use the Nerdy view on a folder to quickly add many variants to a list
- Test the checkout — Always verify the resolved price in the cart to ensure list priority is correct
Anti-Patterns
- ❌ Creating a price list per product (use base variant prices instead)
- ❌ Using price lists for structural price differences (use separate price variants)
- ❌ Overlapping lists with conflicting adjustments on the same scope
- ❌ Forgetting to set an end date on campaign lists (they stay active forever)
Price Variants Reference
Price variants are global definitions that determine the types of prices available across your entire Crystallize catalogue. Each product variant can have a value for every defined price variant.
How Price Variants Work
- Defined globally in Settings → Price Variants in the Crystallize admin
- Once created, available on every product variant in the catalogue
- Each variant has: name, identifier (API key), and currency
- Editors set a price value per variant on each product variant
Variant vs Product Variant — Terminology
Don’t confuse:
- Price variant = a global pricing type (e.g. “Retail USD”) — defined in tenant settings
- Product variant = a specific SKU of a product (e.g. “Red, Size L”) — defined on the product
A product variant can have values for multiple price variants:
Product: "Classic T-Shirt" └── Product Variant: "Red, Size L" (SKU: TSHIRT-RED-L) ├── Price Variant "retail" (USD): $39.99 ← always set (reference / "was" price) ├── Price Variant "sales" (USD): $29.99 ← only set when on sale ("now" price) └── Price Variant "b2b" (USD): $18.00Creating Price Variants
In the Admin UI
- Go to Settings → Price Variants
- Click Add new variant
- Set:
- Name — Human-readable (e.g. “US Dollar Retail”)
- Identifier — Lowercase, hyphenated (e.g.
usd-retail). Used in the API. Cannot be changed after creation. - Currency — ISO 4217 code (e.g.
USD,EUR,NOK,GBP,SEK,DKK,JPY,CHF)
- Save
Via PIM API
mutation { priceVariant { create(input: { tenantId: "your-tenant-id", identifier: "eur-retail", name: "EUR Retail", currency: "EUR" }) { identifier name currency } }}Via Mass Operations
{ "version": "1.0.0", "operations": [ { "intent": "priceVariant/upsert", "identifier": "usd-retail", "name": "USD Retail", "currency": "USD" }, { "intent": "priceVariant/upsert", "identifier": "eur-retail", "name": "EUR Retail", "currency": "EUR" } ]}Setting Prices on Products
Via Mass Operations (product upsert)
Prices are set inside the variants array of a product upsert operation:
{ "intent": "product/upsert", "language": "en", "name": "Classic T-Shirt", "shapeIdentifier": "product", "resourceIdentifier": "TSHIRT-001", "variants": [ { "name": "Classic T-Shirt — Red L", "sku": "TSHIRT-RED-L", "isDefault": true, "priceVariants": [ { "identifier": "retail", "price": 39.99 }, { "identifier": "sales", "price": 29.99 }, { "identifier": "b2b", "price": 18.0 } ] } ]}Via Core API Mutation
mutation UpdateVariantPrice { product { updateVariant( productId: "product-id" language: "en" sku: "TSHIRT-RED-L" input: { priceVariants: [{ identifier: "retail", price: 39.99 }, { identifier: "sales", price: 29.99 }] } ) { ... on Product { id } } }}Querying Prices
Discovery API
query { browse { product(language: en) { hits { name defaultVariant { defaultPrice # The first/default price variant value priceVariants { identifier price currency } } } } }}Getting a Specific Price Variant
query { browse { product(language: en, path: "/shop/t-shirts/classic") { hits { name defaultVariant { priceVariants { identifier price currency } } } } }}In your frontend, filter by identifier:
const retailPrice = variant.priceVariants.find((pv) => pv.identifier === "retail");const salesPrice = variant.priceVariants.find((pv) => pv.identifier === "sales");Naming Convention Guide
Rules
- Identifiers are permanent — Choose carefully; they are used in API queries and cannot be renamed
- Use lowercase with hyphens —
usd-retail, notUSD_RetailorusdRetail - Encode purpose and/or currency — Make identifiers self-documenting
- Be consistent — Pick one pattern and follow it across all variants
Patterns
| Pattern | When to Use | Examples |
|---|---|---|
{currency} | Single purpose, multi-currency | usd, eur, nok |
{purpose} | Single currency, multi-purpose | retail, b2b, members |
{currency}-{purpose} | Multi-currency AND multi-purpose | usd-retail, eur-b2b |
default | Single-market, single-price | default |
Anti-Patterns
- ❌
price1,price2— Not descriptive - ❌
USD_Retail_Price— Too verbose, wrong casing - ❌
norway,sweden— Countries are markets, not price types. Use Price Lists for regional adjustments - ❌
christmas-sale— Temporary campaigns should use Promotions, not permanent variants
Common Scenarios
”Was / Now” Pricing Display
Set up two variants following the convention from the main pricing guide:
retail— The reference price, always set. Displayed as the “was” / compare-at / strikethrough price when a sale is active.sales— The marked-down selling price, only set when the product is on sale. This is the “now” price.
The storefront checks: if sales exists and is lower than retail, show strikethrough. Otherwise, show retail as the current price.
function PriceDisplay({ priceVariants }) { const retail = priceVariants.find((p) => p.identifier === "retail"); const sales = priceVariants.find((p) => p.identifier === "sales");
const onSale = sales && retail && sales.price < retail.price;
return ( <div> {onSale && <span className="line-through text-gray-400">{retail.price}</span>} <span className={onSale ? "text-red-600 font-bold" : ""}> {onSale ? sales.price : retail.price} {retail.currency} </span> </div> );}Multi-Currency Store
Create one variant per currency, resolve which to show based on the customer’s market or preference:
function getDisplayPrice(priceVariants, customerCurrency = "USD") { return ( priceVariants.find((pv) => pv.currency === customerCurrency) || priceVariants.find((pv) => pv.identifier === "default") || priceVariants[0] );}B2B + B2C on Same Store
Use the checkout context / logged-in customer group to determine which variant to display:
function getPrice(priceVariants, isB2B = false) { const identifier = isB2B ? "b2b" : "retail"; return priceVariants.find((pv) => pv.identifier === identifier);}Promotions Reference
Promotions are the final layer in the Crystallize pricing hierarchy. They apply at the cart level — they only become visible during checkout, not on product pages. This keeps your base catalogue pricing clean while enabling flexible campaign mechanics.
How Promotions Work
Unlike price variants and price lists (which set product-level prices), promotions evaluate cart contents at checkout time and apply discounts or adjustments based on rules you define.
Customer browses → Sees base price (from variant + applicable price list)Customer adds items → Cart is hydratedCart is evaluated → Promotions engine checks triggers, targets, limitationsCheckout displays → Adjusted prices with promotion appliedPromotion Components
Every promotion is built from these parts:
1. Mechanism (required)
How the discount is calculated:
| Mechanism | Description | Use Case |
|---|---|---|
| Percentage | % off the original price | ”20% off all shoes” |
| Fixed | Flat amount off | ”$10 off when you spend $50+“ |
| X for Y | Buy X items, pay for Y | ”Buy 3, pay for 2” (cheapest items are free) |
| Cart | Reduce the cart total | ”$20 off the entire order” |
2. Period (optional)
When the promotion is active:
- Start date — When the promotion begins
- End date — When it expires
- Multiple periods — For recurring campaigns (e.g. every December)
- No period — Active immediately and indefinitely
3. Trigger (optional)
What conditions must be met for the promotion to activate:
- Product-based — “3 of product X in the cart”
- Quantity-based — “At least 5 items total”
- Value-based — “Cart total above $100”
- Coupon code — “Enter SUMMER20 at checkout”
- No trigger — Applies to every cart automatically
4. Target (optional)
Which product variants the promotion affects:
- Specific products — Only these SKUs are discounted
- All products — Entire catalogue
- No target — Same as all products
Note: Trigger and target can be the same (“Buy 2 of X, get 1 of X free”) or different (“Buy shoes, get 50% off socks”).
5. Limitations
Control how the promotion behaves:
| Limitation | Description | Example |
|---|---|---|
| Cumulative | Can combine with other discounts? | Yes/No |
| Combine with | Specific promotions it can stack with | Only with “Free Shipping” |
| Repeatable | Applies per qualifying set or once per cart | ”Buy 3 pay 2” repeats for every set of 3 |
| Max usage | Total times it can be applied to a cart | Max 1 application per cart |
| Max per customer | Times each customer can use it | 1 per customer lifetime |
| Quantity per trigger | Items discounted per trigger match | Discount 1 item per trigger |
Common Promotion Patterns
Simple Sitewide Sale
Mechanism: Percentage — 20% offPeriod: Black Friday weekend (Nov 25–28)Trigger: None (automatic)Target: All productsLimitations: Not cumulativeCoupon Code
Mechanism: Percentage — 15% offPeriod: None (always active)Trigger: Coupon code "WELCOME15"Target: All productsLimitations: Max 1 per customerBuy X Get Y Free
Mechanism: X for Y — Buy 3, pay for 2Period: NoneTrigger: 3 items of any product in cartTarget: Same as trigger (cheapest item is free)Limitations: Repeatable (every 3 items), cumulativeFree Shipping Threshold
Mechanism: Cart — Reduce shipping to $0Period: NoneTrigger: Cart total ≥ $75Target: Shipping line itemLimitations: Cumulative (can combine with product discounts)Members-Only Discount
Mechanism: Percentage — 10% offPeriod: NoneTrigger: Customer is in "Members" groupTarget: All productsLimitations: Cumulative, no max usageBundle Discount
Mechanism: Fixed — $15 offPeriod: Summer campaign (Jun 1 – Aug 31)Trigger: Product A AND Product B in cartTarget: Cart totalLimitations: Max 1 per cartPromotions vs Price Lists vs Price Variants
| Feature | Price Variants | Price Lists | Promotions |
|---|---|---|---|
| Scope | Global / all products | Product or market specific | Cart level |
| Visibility | Always (on product pages) | Always (resolved at query/checkout) | Only in cart/checkout |
| Time-limited | No | Yes (via period) | Yes (via period) |
| Conditions | None | Market / customer | Cart contents, coupons |
| Coupon codes | ❌ | ❌ | ✅ |
| Buy X Get Y | ❌ | ❌ | ✅ |
| Per-customer limits | ❌ | ❌ | ✅ |
| Stacking rules | N/A | Priority-based | Cumulative / exclusive |
Applying Promotions in the Cart
Promotions are resolved during cart hydration in the Shop API. The cart context determines which market, customer, and coupon codes are active.
Cart Hydration with a Coupon Code
mutation HydrateCartWithCoupon { cart { hydrate( input: { context: { market: ["eu-retail"], voucherCode: "WELCOME15" } items: [{ sku: "TSHIRT-RED-L", quantity: 2 }, { sku: "HOODIE-BLK-M", quantity: 1 }] } ) { cart { items { variant { sku name } quantity price { gross net currency discount { amount percentage } } } total { gross net currency discount } } } }}The response includes a discount field on each item and on the cart total when a promotion has been applied. If the coupon code is invalid or no promotion matches, the prices are returned without discounts.
Cart Hydration with Automatic Promotions
Promotions without triggers apply automatically — no coupon code needed:
mutation HydrateCart { cart { hydrate(input: { context: { markets: ["us-retail"] }, items: [{ sku: "SNEAKER-WHT-42", quantity: 3 }] }) { cart { items { variant { sku } quantity price { gross net currency discount { amount percentage } } } total { gross net currency discount } } } }}If a “Buy 3, pay for 2” promotion targets sneakers and has no trigger restriction, the discount is applied automatically when the quantity condition is met.
Best Practices
- Keep it simple — Start with 1–3 promotions. Complex stacking rules confuse customers and support teams.
- Always set an end date — Even for “permanent” promotions, set a far-future date for review.
- Test in cart — Always hydrate a test cart to verify the promotion resolves correctly.
- Name for humans — Use descriptive names: “Black Friday 2025 — 20% off shoes” not “Promo 7”.
- Be explicit about stacking — Decide early whether promotions combine. Most stores should default to non-cumulative.
- Use triggers wisely — Automatic promotions (no trigger) apply to every customer. Be intentional.
- Monitor usage — Set
maxUsageandmaxUsagePerCustomerto prevent abuse of coupon codes.
Anti-Patterns
- ❌ Using promotions for permanent price differences (use price variants or price lists)
- ❌ Creating dozens of overlapping promotions (causes unpredictable stacking)
- ❌ Forgetting to test in the cart (promotions are invisible until checkout)
- ❌ Setting cumulative + no max usage on percentage discounts (can result in near-free products)
- ❌ Using promotions for B2B pricing tiers (use price lists with customer groups instead)
Crystallize AI