Offer quantity-based price breaks so wholesale and bulk buyers automatically see lower prices as they add more units to their cart
Volume pricing — also called quantity breaks or tiered pricing — automatically reduces the unit price as order quantities increase. It is a core feature for B2B and wholesale channels, and an effective average-order-value driver for consumer stores ("Add 3 more for 10% off"). Most platforms do not include volume pricing natively; you need an app or plugin. This skill walks through setup on each platform and covers custom implementation for headless storefronts.
| Platform | Recommended Tool | Why |
|---|---|---|
| Shopify | Wholesale Club or Quantity Breaks & Discounts by FORSBERG | Wholesale Club adds a full B2B channel with customer-group pricing; Quantity Breaks handles tiered pricing for B2C with a pricing table on product pages |
| Shopify Plus | Shopify B2B (built in) | Shopify Plus includes native B2B features including company accounts, price lists, and quantity minimums |
| WooCommerce | WooCommerce Dynamic Pricing extension ( | Both plugins support quantity-based tiers, customer-role-specific pricing, and product-page pricing tables |
| BigCommerce | Price Lists (native, Plus plan and above) | BigCommerce's Price Lists feature is designed for B2B volume pricing — assign price lists to customer groups |
| Custom / Headless | Build tier resolution logic calling your platform's pricing API | Full control over tier definitions and cart recalculation |
Option A: Quantity Breaks & Discounts app (B2C-focused)
Option B: Wholesale Club (B2B-focused)
Shopify Plus includes a native B2B channel that replaces the need for a wholesale app:
Using WooCommerce Dynamic Pricing:
| Minimum Qty | Maximum Qty | Discount |
|---|---|---|
| 1 | 4 | 0% |
| 5 | 9 | 10% |
| 10 | 24 | 20% |
| 25 | — | 30% |
Displaying a pricing table on product pages: Both Dynamic Pricing plugins include a pricing table widget that shows quantity break tiers directly on the product page. Configure its appearance in the plugin settings.
BigCommerce's Price Lists feature handles volume pricing natively on Plus and above plans.
Setting up a price list with quantity tiers:
For headless storefronts, implement tier resolution in your pricing service:
interface PriceTier {
minQuantity: number;
maxQuantity?: number; // undefined = no upper limit
type: 'fixed' | 'percentage_off';
value: number; // cents if fixed; percentage (0-100) if percentage_off
customerGroup?: string; // null = all customers
}
async function resolveUnitPrice(
productId: string,
quantity: number,
customerGroup: string | null,
basePrice: number // cents
): Promise<{ unitPriceCents: number; savingsPct: number }> {
// 1. Check for customer-group-specific price list (highest priority)
if (customerGroup) {
const priceListItem = await db.priceLists
.findActive({ product_id: productId, customer_group: customerGroup, min_quantity: { lte: quantity } })
.orderBy('min_quantity', 'desc')
.first();
if (priceListItem) {
const savingsPct = Math.round((1 - priceListItem.price / basePrice) * 100);
return { unitPriceCents: priceListItem.price, savingsPct };
}
}
// 2. Find the best matching general volume tier
const tiers = await db.priceTiers.find({
product_id: productId,
customer_group: customerGroup ?? null,
});
const applicable = tiers.filter(t =>
quantity >= t.minQuantity && (t.maxQuantity === undefined || quantity <= t.maxQuantity)
).sort((a, b) => b.minQuantity - a.minQuantity); // highest-qualifying tier first
const tier = applicable[0];
if (!tier) return { unitPriceCents: basePrice, savingsPct: 0 };
const unitPriceCents = tier.type === 'fixed'
? tier.value
: Math.round(basePrice * (1 - tier.value / 100));
const savingsPct = Math.round((1 - unitPriceCents / basePrice) * 100);
return { unitPriceCents, savingsPct };
}
Recalculate on every cart quantity change:
async function updateCartLinePricing(cartId: string, lineId: string, newQuantity: number) {
const line = await db.cartLines.findById(lineId);
const { unitPriceCents } = await resolveUnitPrice(
line.productId, newQuantity, line.customerGroup, line.basePriceCents
);
await db.cartLines.update(lineId, {
quantity: newQuantity,
unitPriceCents,
lineTotalCents: unitPriceCents * newQuantity,
});
}
Show a pricing table on product pages:
Query the tier breakpoints and display the table:
async function getPricingTable(productId: string, customerGroup: string | null, basePrice: number) {
const breakpoints = [1, 5, 10, 25, 50, 100];
const rows = await Promise.all(breakpoints.map(async qty => {
const { unitPriceCents, savingsPct } = await resolveUnitPrice(productId, qty, customerGroup, basePrice);
return { quantity: qty, unitPriceCents, savingsPct };
}));
// Only show rows where the price actually changes
return rows.filter((row, i) => i === 0 || row.unitPriceCents !== rows[i - 1].unitPriceCents);
}
| Problem | Solution |
|---|---|
| Cart quantity changes but price doesn't update | Recalculate all line item prices on every cart quantity change in real-time, not just at checkout |
| B2B buyer sees retail prices in order confirmation email | Pass customerGroup to all price resolution calls, including order confirmation email rendering |
| A customer in two groups gets inconsistent prices | Resolve by explicit priority: price list > product tier > category tier > base price |
| Pricing table shows stale tiers after an update | Clear your pricing cache when tiers are modified; invalidate by product or tier version |
| Tiered prices not shown on search results pages | Product cards on search/collection pages should also resolve prices server-side for logged-in B2B customers |