Pricing and discounts overview
Overview of the concepts related to product pricing and discounts in Composable Commerce.
Pricing
A Price represents the purchase value of a Product Variant, or SKU, in a specific currency. The uniqueness of a Price is determined by the price scope (currency, country, Customer Groups, or Channels) assigned to it, and only one Price can exist for a price scope.
For example, an SKU can cost €25 in Germany and €30 in Spain. Since the SKU has two Prices defined, the purchase Price for a Customer can depend on the country of purchase. For more information, see Price selection.
In addition to the above, based on your business case, you can use discounts to add flexibility to your product pricing—such as for temporary promotion campaigns. In Composable Commerce, Discounts reduce your effort in using and maintaining discounted and base prices as they can be deactivated when the promotion ends.
For example, you can create a base price for a new mobile phone for Customers in Germany who belong to your Platinum Customer Group. With Product Discounts, you can offer a discounted price for the same combination of dimensions (Germany and Platinum Customer Group). This provides flexibility in being able to utilize the base price together with the discounted price on your web interface, in addition to smooth reporting analytics.
To create an efficient Price model, follow our best practices.
Types of Prices
Composable Commerce offers two ways of storing your product prices. The prices can be embedded in the Product Variants (up to 100
per Product Variant) or stored separately as standalone entities (up to 50 000
per Product Variant). Although a Product can have both types of Prices at the same time, we recommend having only one type of Price for a Product for better performance. The priceMode
of a Product determines the type of Price used for price Selection.
Embedded Prices
Embedded Prices are stored within a ProductVariant, and are associated to it through its prices
field. With Embedded Prices, you can store up to 100
Prices for each Product Variant. For more flexibility with pricing your products with higher limits, see Standalone Prices.
Embedded Prices are managed and queried using the Products API. You can add Prices when creating a Product, or manage Prices of an existing Product using the Add Price, Set Prices, Change Price, or Remove Price update actions.
Changes to Embedded Prices can be staged before being published to the current representation of a Product.
Standalone Prices
Standalone Prices are not embedded within a Product Variant, but are standalone resources that are associated to a ProductVariant through its sku
field. With Standalone Prices, you can store up to 50 000
Prices for each Product Variant.
Standalone Prices are managed and queried using the Standalone Prices API, independent from your Products, thus contributing to better query performance.
Changes to Standalone Prices can be staged with StagedStandalonePrice before being published.
External Prices
External prices are stored and maintained in a system external to Composable Commerce, except when using Custom Objects for storing your pricing data. Since external prices are not part of the product catalog, they are not available on the Product Projection Search API as well as the Product Search API for filtering, faceting, or sorting products by price.
You can set an external price for the Line Item when creating or updating a Cart using the following actions: Add LineItem, Remove LineItem, Change LineItem Quantity, Set LineItem Price, or Set LineItem TotalPrice.
If the LineItem's priceMode
is ExternalPrice
, set the externalPrice
value. The totalPrice
of the LineItem is automatically calculated by multiplying the externalPrice
by the quantity.
If the LineItem's priceMode
is ExternalTotal
, set the externalTotalPrice
value (unit price multiplied by the quantity). The totalPrice
of the LineItem is set from the value defined in externalTotalPrice
.
Your code should be able to find the correct prices to display on your storefront and set the Line Item price when it's added to a Cart.
Consider using external prices in the following cases:
- Pricing data exceeds the limit of
50 000
Prices for a Product Variant. - Pricing data is stored externally in a Product Information Management (PIM).
In such cases, to display the Price on your storefront for a Product Variant or for a Line Item in the Cart, you would need to make API calls to the external system. Alternatively, you can also import your prices into Composable Commerce as Standalone Prices or Embedded Prices. - Pricing data is dynamic and changes frequently or is calculated on the fly based on some external values.
If pricing fluctuates frequently or is calculated based on external factors (like currency conversion rates), you need to obtain the data through API calls to the external system before displaying the prices on your storefront. - Pricing data is stored in some datastore that is colocated with your application code.
In such cases, to display Prices on your storefront or for Line Items in the Cart, you must make database calls rather than external API calls. - Pricing data is stored as Custom Objects.
Although Custom Objects are stored within Composable Commerce and you can access the data through commercetools' API calls, the Prices are still considered external as your code needs to access the data when you need to display or add pricing information into the Cart. Additionally, you cannot take advantage of all the price selection logic used by Embedded and Standalone Prices.
Tiered pricing
Tiered pricing can be useful in bulk-purchasing scenarios to apply a different pricing when a certain quantity of an item is added and ordered from a cart. For example, if a product's base price is €5, when 100 items are added and ordered from the cart, a tiered Price of €3 can be used for each item. Tiered pricing is an alternate way to discount prices without using Product Discounts.
When the PriceTier minimumQuantity
is reached, the Price tier is applied for the entire quantity of a Product Variant added as Line Item to a Cart. If no Price tier is found for the Order quantity, the base Price is used.
The Price tier is applied per Line Item of the Product Variant. For example, if the same Product Variant appears in the same Cart as several Line Items (which can be achieved by different values of a Custom Field on the Line Items), each Line Item must reach the minimum quantity to get the Price tier.
Both Embedded Prices and Standalone Prices provide support for tiered pricing.
Price selection
Although you can specify product prices with different price scopes to serve all customers, you would want the most suitable price selected for your customer. The price displayed on a product detail page is determined using the Product price selection parameters, and the price calculated on a Cart is determined using the Line Item price selection parameters.
Product price selection: When displaying a price on a product detail page, the price is determined by the Product Projections API and Product Projection Search endpoint based on the following parameters:
priceCurrency
: maps tovalue.currencyCode
of Price or StandalonePricepriceCustomerGroup
: maps tocustomerGroup
of Price or StandalonePricepriceChannel
: maps tochannel
of Price or StandalonePricepriceCountry
: maps tocountry
of Price or StandalonePrice
The returned ProductVariant
price
field—added in API responses—contains the Price that matches most of the Price-selection parameters.Line Item price selection: When you create a Cart or add a LineItem to a Cart, the price is determined by the Carts API based on the following parameters:
- Cart
currency
: maps tovalue.currencyCode
of Price or StandalonePrice - Cart
customerGroup
: maps tocustomerGroup
of Price or StandalonePrice - LineItemDraft
distributionChannel
: maps tochannel
of Price or StandalonePrice - Cart
country
: maps tocountry
of Price or StandalonePrice
- Cart
Composable Commerce selects the price for Products and Line Items based on the following fallback logic.
Fallback logic
To consider what type of Price must be used for a Product Variant, APIs check the Product priceMode
value. You can set the priceMode
when creating a Product; for existing Products, use the Set PriceMode update action. If the priceMode
is not set, APIs use Embedded Prices as they consider the Product to have the Embedded
ProductPriceMode, as a fallback.
A Price for a specific currency is selected in the below-mentioned order with Customer Group having the highest priority, followed by channel, and then country:
- Checks for a Price for the Customer Group, channel, and country
- Checks for a Price for the Customer Group and channel for all countries
- Checks for a Price for the Customer Group and country for all channels
- Checks for a Price for the Customer Group for all channels and countries
- Checks for a Price for the channel and country for all Customer Groups
- Checks for a Price for the channel for all Customer Groups and countries
- Checks for a Price for the country for all Customer Groups and channels
- Checks for a Price for any Customer Group, channel, and country
If a Price is not found in one step, the next steps are applied until the Price is found. If no Price is found with an exact match, wildcards (all countries, channels, and CustomerGroups) are used.
For all the Price-selection steps, Prices with a validity period are checked before Prices without a validity period. If a currently valid Price is found, it is used first.
The Price is shown only when the timestamp of the request is within validFrom
and validUntil
range of Embedded Price or StandalonePrice.
If the Price has PriceTier, the Price tier valid for the Line Item quantity is selected. However, the tiered price will be ignored if the Price is already discounted (by a Product Discount). If no Price tier can be used, the base Price is used.
Scoped Price search
Scoped Price search—that includes filter, facet, and sort options—only works with Embedded Prices. Using these options on Products with Standalone
ProductPriceMode yields inconsistent results, as only Embedded Prices are taken into account.
You can use the Price-selection parameters on the Product Projection Search endpoint to search for a Price with the exact scope as specified with the Price-selection parameters.
In case of a full match—where a Price matching all Price-selection parameters provided with the search request exists—the ProductVariants returned with the search result contains the scopedPrice
field. In case of a partial match, this field is not present.
Scoped Price search does not have a fallback behavior, and does not take the Price validity dates into consideration. To ensure only Products with valid Prices are returned, remove Prices that are no longer valid. For Prices to become valid at a later time, do not publish them before their validFrom
date, and search for the current Product Projection.
To illustrate the difference to price selection, consider a Product Variant with two Embedded Prices defined:
- Embedded Price 1 with
country
:US
,value
: {centAmount
:1000
,currencyCode
:USD
} - Embedded Price 2 with
country
:US
,value
: {centAmount
:800
,currencyCode
:USD
},customerGroup
:B2B
Consider that the Price-selection parameters, priceCountry
: US
and priceCustomerGroup
: B2C
, are provided in the Product Projection Search request. Since these Price-selection parameters only match partially, the ProductVariant only contains the price
field with 'Embedded Price 1' defined for the Product Variant, and not the scopedPrice
field.
Discounts
Discounts can assist in creating efficient pricing solutions for your business. For example, one best practice would be to take advantage of Product Discounts to display sale prices for your Products instead of using the available pricing solutions, which are better suited for storing base prices.
Typically, B2C business models have simple base pricing structures and rely on frequent promotional activities to drive sales. In contrast, B2B business models often have more complex pricing that involves high precision (sub-cent) prices and tiered pricing. Discounts play a less significant role in B2B as purchasing decisions are generally made by groups, reflecting a more structured decision-making process.
Composable Commerce offers the following types of Discounts:
Product Discounts
Product Discounts can be used for product or catalog discounts to display a discounted Price for your Products before they are added to a Cart, for example, on product detail pages (PDPs) or product listing pages (PLPs).
Only one Product Discount applies to a Product at any given time. If more than one active Product Discount matches a Price, the ProductDiscount sortOrder
determines which one applies. For published Products, Product Discounts apply based on the current ProductData. For unpublished Products, the staged ProductData is used to calculate the Discount.
Product Discounts can apply to both Embedded Prices and Standalone Prices:
- For Embedded Prices, the system stores Product Discounts in the Price
discounted
field. - For Standalone Prices, the Product Discounts API uses the StandalonePrice
discounted
field in its current and staged representation.
Product Discounts are managed and queried using the Product Discounts API. For cases where the base prices are stored externally or when Composable Commerce logic cannot support your discounting requirements, you can use external discounts. In these cases, the discounts are calculated by an external system and you need to set the discounted amounts in Composable Commerce. To use external discounts, set the ProductDiscountDraft value
to external
.
Cart Discounts
Cart Discounts can be used to discount different elements of a Cart, some of which include:
- A discount on any item in a cart with CartDiscountLineItemsTarget or CartDiscountCustomLineItemsTarget
- A discount on the shipping cost of a cart with CartDiscountShippingCostTarget
- A discount on a cart's total with CartDiscountTotalPriceTarget
- A 'Buy X, get Y free' discount with CartDiscountValueGiftLineItemDraft
- A 'Buy X items, get Y of them at a discounted rate' discount with MultiBuyLineItemsTarget or MultiBuyCustomLineItemsTarget. Note that you can only apply a percentage-off discount on eligible items.
Cart Discounts are recalculated every time a Discount Code, LineItem, or CustomLineItem is added or removed from a Cart, or when an Order is created from a Cart.
Unlike Product Discounts, multiple Cart Discounts can apply on a Cart at any given time, but in a ranked order. This includes Discount Codes applied during checkout, which are associated with Cart Discounts. You can avoid applying further Cart Discounts to the Cart by setting the stackingMode
to StopAfterThisDiscount
on the CartDiscount that must be applied last.
Cart Discounts can be defined globally for a Project, or be specific to one or more Stores in a Project. In the latter case, the Discount is only applied to Carts that belong to one of the Stores referenced in the CartDiscount stores
array. If the array is empty or unspecified, the Discount applies (globally) to all Carts. To apply a Cart Discount to only a single Cart or Order, you can use Direct Discounts.
Cart Discounts are managed and queried using the Cart Discounts API.
Discount Codes
Discount Codes can be used to offer additional Cart Discounts to eligible Customers by use of unique codes during checkout. With Discount Codes, you can also control the number of times they apply on a Cart, and this limit can be configured to apply on a Customer's Cart or any Cart. A Cart can have up to 10
Discount Codes, and each Discount Code can include up to 10
Cart Discounts.
Discount Codes are not ranked like Cart Discounts, but the order in which they apply on a Cart depends on the ranking of the Cart Discounts (the Discount Codes are associated to). For example, consider a scenario where the following discounts apply on a Cart:
- SUMMER SALE: a Cart Discount that discounts the Cart by €10 with a CartDiscount
sortOrder
of 0.05 - MYFIRSTPURCHASE: a Discount Code associated to a Cart Discount, NEW CUSTOMERS, that discounts the Cart by €5 with a CartDiscount
sortOrder
of 0.1
Thus, based on the sortOrder
, Composable Commerce prioritizes NEW CUSTOMERS before SUMMER SALE, and first applies a discount of €5 followed by €10.
Discount Codes are managed and queried using the Discount Codes API.
Direct Discounts
Direct Discounts are used to represent Cart Discounts that can be applied on Quotes, and are associated only with a single Cart or Order. They are always active and valid, and have the default StackingMode, Stacking
. Unlike Cart Discounts, they do not have a sorting order, and apply in the order listed in the directDiscounts
array of a Cart or Order.
If a Direct Discount is applied to a Cart or Order, any matching Cart Discounts in the Project are ignored and do not apply. Cart Discounts using Discount Codes cannot be applied as well, as Direct Discounts and Discount Codes are mutually exclusive in a Cart or Order.
Customer-specific pricing and discounts
Composable Commerce also lets you define prices and discounts specific to Customers using Customer Groups or Business Units. For more information, see Customer-specific products and prices.
Additionally, as Buyers, Customers can also negotiate prices with a Seller using Quotes. For more information, see Quotes overview.