Skip to content

pricing

Terminal window
npx skills add https://github.com/crystallizeapi/ai --skill pricing

Crystallize 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:

  1. Which price variant applies (based on context)
  2. Whether a price list overrides that variant (based on market, customer group, time period)
  3. 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 groups

Price 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 TypeRename default toCurrencyRationale
B2C single marketretailYour main currencyThe standard consumer price
B2C multi-marketretail (main market currency)Main market currencyThe primary market’s retail price
B2B single marketb2b or wholesaleYour main currencyYour main B2B list price
B2B + B2Cretail (main market)Main market currencyRetail 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:

PatternExample IdentifierExample NameCurrency
Single marketdefaultDefaultUSD
Currency-basedusd, eur, nokUS Dollar, Euro, Norwegian Kronerespective
Purpose-basedretail, sales, b2bRetail, Sales, B2Bsame
Combinedusd-retail, eur-b2bUSD Retail, EUR B2Brespective

Simple B2C (single market)

Price Variants:
└── default (USD) — The only price, used everywhere

When: 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 sales

When: 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 to retail
  • EU market: Check sales-eur → fall back to retail-eur
  • UK market: Check sales-gbp → fall back to retail-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 price

When: 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 price

When: 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 market

When: 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 price

When: 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 ComplexityTypical CountVariants
Simple single-market1default
B2C with sales pricing2retail + sales
Multi-currency B2C2–5One per currency
B2C + B2B single currency2–3retail + sales + b2b
Enterprise multi-market4–8Purpose × currency

Common Mistakes

  1. Creating a variant per country — Use Price Lists for regional adjustments instead
  2. Leaving “default” named as “default” — Always rename it to reflect its purpose (e.g. retail for B2C, b2b for B2B). The name “default” tells nobody what the price represents.
  3. 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.
  4. 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 sales price variant instead.
  5. Inconsistent naming — Pick a convention ({purpose}-{currency} or {purpose}) and stick with it across all variants
  6. 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
  7. 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)

ScenarioUse Price Variant?Use Price List?
USD vs EUR base prices✅ YesNo
B2B vs Retail price type✅ YesNo
”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

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: Nordics

Customer-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 Corp

Seasonal Campaigns

Price Lists:
├── "Summer Sale EU" — 20% off retail, Market: EU, Period: Jun–Aug
└── "Black Friday Global" — Specific prices, Period: Nov 25–28

Markets

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 context

Multi-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 businesses

How 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

MechanismDescriptionExample
Percentage% off the price”20% off all shoes”
FixedFlat amount off”$10 off orders over $50”
X for YBuy X, pay for Y”Buy 3, pay for 2”
CartReduce cart total”$20 off the entire cart”

Promotion Components

  1. Mechanism — How the discount is calculated (percentage, fixed, X-for-Y, cart)
  2. Period (optional) — When the promotion is active (start/end dates, can recur)
  3. Trigger (optional) — What activates it (e.g. “3 items of product X in cart”)
  4. Target (optional) — Which products are affected (if empty, applies to all)
  5. Limitations — Cumulative rules, max usage, per-customer limits

When to Use Promotions vs Price Lists

ScenarioPromotionsPrice Lists
Time-limited discountsPossible 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

  1. Business Model

    • “Is this B2C, B2B, or both?”
    • “Do you have tiered pricing for different customer groups?”
  2. 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?”
  3. 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?”
  4. 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?”
  5. 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:

  1. Price Variants — The base layer: how many, naming, currencies
  2. Markets — If multi-region: how to segment
  3. Price Lists — Regional/customer overrides (if needed)
  4. Promotions — Campaign strategy (if needed)
  5. 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 only
After: Price variants + Promotions

Keep 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


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:

  1. Go to Settings → Markets
  2. Click Add market +
  3. Enter a name (e.g. “Norway B2B”) and identifier (e.g. norway-b2b)
  4. 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:

  1. Which price lists are evaluated
  2. Which promotions apply
  3. 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:

  1. Go to Special Prices → Price Lists
  2. Click Add new
  3. 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

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

TypeDescriptionExample
PercentageAdjust up or down by %-10% = 10% discount
RelativeAdd or subtract a fixed amount-5 = $5 off
AbsoluteSet a specific price25.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: £89

Customer 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: €56

Time-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: eu

Individual 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:

  1. Most specific target wins — Individual customer > customer group > market > global
  2. Active period — Only lists within their defined period are evaluated
  3. Product scope — Product-specific lists override “all products” lists

Best Practices

  1. Name descriptively — Include the target context in the name: “EU B2B Q1 2025” not “Price List 3”
  2. Limit overlap — Avoid having many price lists targeting the same market+variant combination
  3. Use periods — Even for “permanent” adjustments, set a far-future end date so they can be reviewed
  4. Bulk select products — Use the Nerdy view on a folder to quickly add many variants to a list
  5. 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.00

Creating Price Variants

In the Admin UI

  1. Go to Settings → Price Variants
  2. Click Add new variant
  3. 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)
  4. 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

  1. Identifiers are permanent — Choose carefully; they are used in API queries and cannot be renamed
  2. Use lowercase with hyphensusd-retail, not USD_Retail or usdRetail
  3. Encode purpose and/or currency — Make identifiers self-documenting
  4. Be consistent — Pick one pattern and follow it across all variants

Patterns

PatternWhen to UseExamples
{currency}Single purpose, multi-currencyusd, eur, nok
{purpose}Single currency, multi-purposeretail, b2b, members
{currency}-{purpose}Multi-currency AND multi-purposeusd-retail, eur-b2b
defaultSingle-market, single-pricedefault

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 hydrated
Cart is evaluated → Promotions engine checks triggers, targets, limitations
Checkout displays → Adjusted prices with promotion applied

Promotion Components

Every promotion is built from these parts:

1. Mechanism (required)

How the discount is calculated:

MechanismDescriptionUse Case
Percentage% off the original price”20% off all shoes”
FixedFlat amount off”$10 off when you spend $50+“
X for YBuy X items, pay for Y”Buy 3, pay for 2” (cheapest items are free)
CartReduce 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:

LimitationDescriptionExample
CumulativeCan combine with other discounts?Yes/No
Combine withSpecific promotions it can stack withOnly with “Free Shipping”
RepeatableApplies per qualifying set or once per cart”Buy 3 pay 2” repeats for every set of 3
Max usageTotal times it can be applied to a cartMax 1 application per cart
Max per customerTimes each customer can use it1 per customer lifetime
Quantity per triggerItems discounted per trigger matchDiscount 1 item per trigger

Common Promotion Patterns

Simple Sitewide Sale

Mechanism: Percentage — 20% off
Period: Black Friday weekend (Nov 25–28)
Trigger: None (automatic)
Target: All products
Limitations: Not cumulative

Coupon Code

Mechanism: Percentage — 15% off
Period: None (always active)
Trigger: Coupon code "WELCOME15"
Target: All products
Limitations: Max 1 per customer

Buy X Get Y Free

Mechanism: X for Y — Buy 3, pay for 2
Period: None
Trigger: 3 items of any product in cart
Target: Same as trigger (cheapest item is free)
Limitations: Repeatable (every 3 items), cumulative

Free Shipping Threshold

Mechanism: Cart — Reduce shipping to $0
Period: None
Trigger: Cart total ≥ $75
Target: Shipping line item
Limitations: Cumulative (can combine with product discounts)

Members-Only Discount

Mechanism: Percentage — 10% off
Period: None
Trigger: Customer is in "Members" group
Target: All products
Limitations: Cumulative, no max usage

Bundle Discount

Mechanism: Fixed — $15 off
Period: Summer campaign (Jun 1 – Aug 31)
Trigger: Product A AND Product B in cart
Target: Cart total
Limitations: Max 1 per cart

Promotions vs Price Lists vs Price Variants

FeaturePrice VariantsPrice ListsPromotions
ScopeGlobal / all productsProduct or market specificCart level
VisibilityAlways (on product pages)Always (resolved at query/checkout)Only in cart/checkout
Time-limitedNoYes (via period)Yes (via period)
ConditionsNoneMarket / customerCart contents, coupons
Coupon codes
Buy X Get Y
Per-customer limits
Stacking rulesN/APriority-basedCumulative / 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

  1. Keep it simple — Start with 1–3 promotions. Complex stacking rules confuse customers and support teams.
  2. Always set an end date — Even for “permanent” promotions, set a far-future date for review.
  3. Test in cart — Always hydrate a test cart to verify the promotion resolves correctly.
  4. Name for humans — Use descriptive names: “Black Friday 2025 — 20% off shoes” not “Promo 7”.
  5. Be explicit about stacking — Decide early whether promotions combine. Most stores should default to non-cumulative.
  6. Use triggers wisely — Automatic promotions (no trigger) apply to every customer. Be intentional.
  7. Monitor usage — Set maxUsage and maxUsagePerCustomer to 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