Advanced license required
Features described on this page require the Xperience by Kentico Advanced license tier.
The free shipping promotion framework allows you to create custom promotion rules that grant free shipping during price calculation. Unlike catalog and order discounts, which reduce a price, free shipping eliminates the shipping cost entirely when the cart meets the rule's conditions. Free shipping always removes the shipping price in full, partial discounts are not possible.
Use free shipping to implement:
Free shipping doesn't compete with other promotion types
A customer can receive free shipping together with any applicable catalog and order discounts.
Free shipping promotions share the same configuration and evaluation framework as catalog and order discounts, so the following capabilities work identically:
A promotion rule defines the logic for determining whether an order qualifies for free shipping. You implement promotion rules as classes that inherit from FreeShippingPromotionRule.
The rule receives the calculation data for the whole cart or order – the cart items with their prices after other discounts, the calculation mode (shopping cart or checkout), and, in checkout, the shipping method the customer selected. Based on this data, the rule answers a single question: does the order qualify for free shipping? If it does, the rule can also name the specific products that earned the reward. In the shopping cart mode, the shipping method is not selected yet and the rule checks whether free shipping is available for any free shipping method.
A promotion candidate represents a reward that a free shipping rule grants to an order. When a promotion rule determines that the order qualifies, it returns a FreeShippingPromotionCandidate containing:
TriggerProductIdentifiers – An optional list of the products that caused the cart to qualify. Leave this empty for a whole-cart condition, such as a minimum order amount. See Restrict the reward to specific products.When the cart doesn't qualify, return null instead of a candidate.
Unlike CatalogPromotionCandidate and OrderPromotionCandidate, FreeShippingPromotionCandidate doesn't carry a discount amount – the reward is always the full shipping price.
Promotion rules can define configurable properties that store managers set when creating promotions in the administration interface. Properties are defined in a separate class implementing IPromotionRuleProperties and are decorated with editing components to generate the UI. More details are provided in Define promotion rule properties.
When multiple free shipping promotions qualify for the same cart, the system selects one to credit: a promotion activated by the customer's coupon code wins if any exists, otherwise the newest qualifying promotion is used. Every qualifying promotion is still evaluated and returned as a candidate, but only the selected one is marked Applied. The reward for free shipping promotions is always the full shipping price. Only the selected promotion is persisted when the order is created.
Implementing a free shipping promotion rule involves creating two components:
IPromotionRuleProperties or inherits from FreeShippingPromotionRuleProperties.FreeShippingPromotionRuleBase or FreeShippingPromotionRule.These two components work together: the properties class stores the configuration, and the logic class uses those configured values to evaluate the cart during price calculation.
Sample implementation
See DancingGoatFreeShippingPromotionRule.cs in the Dancing Goat project template for a sample rule implementation.
The system provides FreeShippingPromotionRuleProperties as a base class with common properties already defined and configurable via the administration interface:
You can inherit from this base class to include these standard fields and add your own custom properties. Use editing component annotations to generate the input fields for each property:
using System.Collections.Generic;
using CMS.ContentEngine;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.DigitalCommerce;
namespace Codesamples.Commerce;
public class ProductFreeShippingProperties : FreeShippingPromotionRuleProperties
{
// Base class properties include: MinimumRequirementValueType, MinimumRequirementValue,
// ShippingMethodScope, ShippingMethods
// Add custom properties for your business logic
// For example, a property allowing editors to select products that will count toward the minimum requirement
[ContentItemSelectorComponent(
typeof(ProductSchemaFilter),
Label = "Eligible products",
Order = 1)]
// Not visible when no minimum requirements are needed to qualify for the shipping
[VisibleIfNotEqualTo(nameof(MinimumRequirementValueType), "none")]
public IEnumerable<ContentItemReference> EligibleProducts { get; set; } = [];
}This example uses ProductSchemaFilter (an implementation of IReusableFieldSchemasFilter) to scope the items offered in the selector, so that only content items that are products in the store appear. For other ways to scope a content item selector, see Reference - Admin UI form components.
using System.Collections.Generic;
using Kentico.Xperience.Admin.Base.FormAnnotations;
public class ProductSchemaFilter : IReusableFieldSchemasFilter
{
IEnumerable<string> IReusableFieldSchemasFilter.AllowedSchemaNames => new List<string> { IProductFields.REUSABLE_FIELD_SCHEMA_NAME };
}The base class properties use negative Order values (e.g., -190, -90, -80) to ensure they appear at the top of the form, grouped under their own categories. Keep this in mind when positioning custom properties.
Alternatively, you can directly implement IPromotionRuleProperties, bypassing the inheritance entirely, if you need full control over all configuration properties used by the rule.
The framework provides two base classes for free shipping promotion rules:
FreeShippingPromotionRuleProperties. Provides built-in helper methods that work with the base properties, including minimum requirement evaluation (whole-order minimum, minimum number of items, minimum single item price) and shipping method scoping.IPromotionRuleProperties directly. You must implement all eligibility logic yourself.Both base classes use the same generic type parameters:
TPromotionRuleProperties – The rule's properties class. Must inherit from FreeShippingPromotionRuleProperties for FreeShippingPromotionRule, or implement IPromotionRuleProperties for FreeShippingPromotionRuleBase.TPriceCalculationRequest – The price calculation request type.TPriceCalculationResult – The price calculation result type.For rule registration, see Register the promotion rule.
When your properties class inherits from FreeShippingPromotionRuleProperties, FreeShippingPromotionRule provides you access to the following built-in methods and features:
MeetsMinimumRequirement(items) – Evaluates the given cart items against MinimumRequirementValueType and MinimumRequirementValue from the base properties, which can be configured by store managers in the administration. Price and quantity requirements count the item price after all discounts, since the free shipping step runs after order promotions. See Define promotion rule properties for the breakdown of what the MinimumRequirementValueType property can represent.GetItemsMeetingMinimumRequirement(items) – Returns the items that earned the reward. For a minimum single item price requirement, only the items whose own price meets the threshold; for the other requirement types (none, minimum purchase amount, minimum quantity), the whole given set, since those are evaluated on the total across all items rather than any one item. Useful for building TriggerProductIdentifiers in the promotion candidate.IsApplicable with automatic shipping method scoping – FreeShippingPromotionRule overrides IsApplicable so the promotion is evaluated only when the shipping method selected in the order is covered by the promotion, as determined by ShippingMethodScope and ShippingMethods from the base properties. In the shopping cart mode no shipping method is selected yet, so the check passes and the evaluation always runs. See Filter promotion applicability if you need additional conditions – call base.IsApplicable from your override to keep the shipping method check.GetDiscountValueLabel() – Already overridden to return a localized "Free shipping" label for display. Override it again only if you need custom wording.The following sample demonstrates a minimal rule that uses the base properties:
using System;
using System.Collections.Generic;
using System.Linq;
using CMS.Commerce;
using Kentico.Xperience.Admin.DigitalCommerce;
namespace Codesamples.Commerce;
public class MinimumOrderFreeShippingRule
: FreeShippingPromotionRule<FreeShippingPromotionRuleProperties,
PriceCalculationRequest, PriceCalculationResult>
{
public const string IDENTIFIER = "Acme.MinimumOrderFreeShipping";
public override FreeShippingPromotionCandidate GetPromotionCandidate(
IPriceCalculationData<PriceCalculationRequest, PriceCalculationResult> calculationData)
{
if (!MeetsMinimumRequirement(calculationData.Result.Items))
{
// The cart doesn't qualify -- no reward
return null;
}
// For a minimum item price requirement, the qualifying items are trigger products.
// For the other requirement types, the whole cart met the requirement together, so
// leaving TriggerProductIdentifiers empty stores the reward once at the order level.
List<ProductIdentifier> triggerProducts;
if (Properties.MinimumRequirementValueType.ToString().Equals("ItemPrice", StringComparison.OrdinalIgnoreCase))
{
triggerProducts = GetItemsMeetingMinimumRequirement(calculationData.Result.Items)
.Select(item => item.ProductIdentifier)
.ToList();
}
else
{
triggerProducts = [];
}
return new FreeShippingPromotionCandidate
{
TriggerProductIdentifiers = triggerProducts
};
}
}Register the rule using RegisterPromotionRuleAttribute with the following parameters:
PromotionType.Shipping to indicate this is a free shipping promotion.Without registration, the rule doesn't appear in the administration interface.
[assembly: RegisterPromotionRule<MinimumOrderFreeShippingRule>(
MinimumOrderFreeShippingRule.IDENTIFIER,
PromotionType.Shipping,
"Minimum order -- free shipping")]Override the IsApplicable method to add high-level conditions that determine whether the promotion should be evaluated at all. This method runs once per promotion before GetPromotionCandidate, making it useful for performance optimization when you can skip the entire promotion early.
Built-in shipping method validation
When using FreeShippingPromotionRule, the base class IsApplicable implementation automatically validates the shipping method scope based on ShippingMethodScope and ShippingMethods properties. Unlike OrderPromotionRule, it does not validate the minimum purchase requirement – call MeetsMinimumRequirement explicitly inside GetPromotionCandidate instead, as shown above. You only need to override IsApplicable if you have additional custom conditions.
The evaluation pipeline works as follows:
IsApplicable is called once. If it returns false, the promotion is skipped entirely.GetPromotionCandidate is called to determine if the cart qualifies.GetPromotionCandidate returns a candidate qualifies. The system then selects one of the qualifying candidates to credit (see Promotion selection logic).Use IsApplicable for checks that apply to the entire promotion context. For example, to restrict a promotion to registered members while keeping the built-in shipping method validation:
using System.Threading;
using System.Threading.Tasks;
using CMS.Commerce;
// ...
public override async Task<bool> IsApplicable(
IPriceCalculationData<PriceCalculationRequest, PriceCalculationResult> calculationData,
CancellationToken cancellationToken)
{
BuyerIdentifier buyerIdentifier = calculationData.Request.BuyerIdentifier;
// Checks if the buyer is a registered member
if (buyerIdentifier == null || buyerIdentifier.Kind != BuyerIdentifierKind.Member)
{
return false;
}
// Keeps the automatic shipping method check
return await base.IsApplicable(calculationData, cancellationToken);
}A free shipping promotion doesn't have to depend on the whole cart. To grant free shipping only when the cart contains specific products or products from specific categories, first add a property that lets store managers select the eligible products or categories, such as EligibleProducts in the Define promotion rule properties example.
EligibleProducts holds GUID-identified content item references, while cart items are keyed by the int-identified ProductIdentifier – the two can't be compared directly. Match on product data instead, extended with the product's content item GUID – see Customize price calculation:
using System;
using CMS.Commerce;
public record ProductDataWithContentItemGuid : ProductData
{
public Guid ContentItemGuid { get; init; }
}Then evaluate the scoped cart items in GetPromotionCandidate and return the matching products as TriggerProductIdentifiers:
using System.Linq;
// ...
public override FreeShippingPromotionCandidate GetPromotionCandidate(
IPriceCalculationData<PriceCalculationRequest, PriceCalculationResult> calculationData)
{
var scopeItems = calculationData.Result.Items
.Where(item => item.ProductData is ProductDataWithContentItemGuid product
&& Properties.EligibleProducts.Any(eligible => eligible.Identifier == product.ContentItemGuid));
if (!MeetsMinimumRequirement(scopeItems))
{
return null;
}
return new FreeShippingPromotionCandidate
{
TriggerProductIdentifiers = GetItemsMeetingMinimumRequirement(scopeItems)
.Select(item => item.ProductIdentifier)
.ToList()
};
}Recording trigger products matters beyond eligibility – when the order is created, the system stores the granted free shipping reward once per trigger item instead of once for the whole order. This lets a later return or refund of a trigger item identify the free shipping that the item earned. Leave TriggerProductIdentifiers empty for whole-cart conditions (such as a minimum order amount that isn't scoped to specific products), and the reward is stored once at the order level instead.
Sample implementation
See DancingGoatFreeShippingPromotionRule.cs and DancingGoatFreeShippingProperties.cs in the Dancing Goat project template for a real implementation that scopes the reward by category, product, or tag.
After price calculation, you can access promotion information from the result to display eligibility to customers or for reporting purposes.
Granted free shipping promotion candidates are stored in the PromotionData.FreeShippingPromotionCandidates collection on the IPriceCalculationResult returned by price calculation. Each candidate is wrapped in IPriceCalculationPromotionCandidate<FreeShippingPromotionCandidate>, exposing the promotion ID, the coupon code that activated it (if any), and the candidate itself:
var qualifiesForFreeShipping = calculationResult.PromotionData.FreeShippingPromotionCandidates.Count > 0;
foreach (IPriceCalculationPromotionCandidate<FreeShippingPromotionCandidate> candidate in calculationResult.PromotionData.FreeShippingPromotionCandidates)
{
var promotionId = candidate.PromotionID;
var triggerProducts = candidate.PromotionCandidate.TriggerProductIdentifiers;
var couponCode = candidate.CouponCode;
}