1. Promotions, sale prices, and discounts

Promotions

Custom rule-based expressions for order promotions provide extensive flexibility to meet diverse business requirements. This implementation of Flexibility over Features enables developers to create customized if-then statements that solve more scenarios than fixed promotion types.

Promotion structure

Core components

Every promotion requires two expressions:

  1. EligibleExpression: Evaluates order state

    • Returns true/false
    • Determines promotion applicability
  2. ValueExpression: Calculates discount

    • Returns monetary value
    • Subtracts from order subtotal

Available properties

  • order: Matches Order model from v1/orders
  • Includes extended properties (xp)
  • items: Provides collection functions

Collection functions

Line item operations

  • items.any(): True if any item matches filter
  • items.all(): True if all items match filter
  • items.quantity(): Sum of matching quantities
  • items.count(): Number of matching line items
  • items.total(): Dollar amount comparison

Product categorization

  • product.incategory(): Direct category assignment
  • product.inparentcategory(): Category or child assignment

Array operations

  • contains(): Value exists in array
  • count(): Matching array items
  • any(): Any array item matches
  • all(): All array items match

Property operations

  • in(): Value exists in list
  • Supports comma-delimited lists

Implementation examples

Array operations

javascript
// Check array content
order.xp.myarray.contains('value2')

// Count array items
order.xp.myarray.count() = 3

// Check specific value
order.xp.myarray.any(item = 'four')

// Pattern matching
order.xp.Tags.all(item = 'tag*') = true

// Product tag check
items.any(Product.xp.Tags.contains('value2')

// Numeric array check
item.Product.xp.NumberArray.contains(23)

// Array value match
item.product.xp.myarray.any(item = 20)

// Pattern count
item.product.xp.Tags.count(item = 'tag*') = 3

Expression operators

Comparison operators

  • Equal: =
  • Less than: <
  • Greater than: >
  • Less/equal: <=
  • Greater/equal: >=

Logical operators

  • And: and
  • Or: or
  • Not: not

Mathematical operators

  • Addition: +
  • Subtraction: -
  • Multiplication: *
  • Division: /
  • Modulo: %

Helper functions

Minimum value

javascript
min(200, order.Total)
  • Returns smaller argument
  • Type conversion rules apply
  • Integer vs decimal handling

Maximum value

javascript
max(200, order.Total)
  • Returns larger argument
  • Type conversion rules apply
  • Integer vs decimal handling

Conditional logic

javascript
ifs(condition1, value1, condition2, value2, default)
  • Evaluates multiple conditions
  • Returns matching value
  • Last parameter is default

Rounding

javascript
round((order.Total * .1), 0)
  • Rounds to specified decimals
  • Supports expressions
  • Precise control

Promotion limits

Line item promotions

Requires LineItemLevel: true

Item limit

  • ItemLimitPerOrder: Maximum items
  • Applies discount per item
  • Excludes quantity limit

Quantity limit

  • QuantityLimitPerOrder: Maximum quantity
  • Applies discount per unit
  • Excludes item limit

Sort order

  • ItemSortBy: Property-based sorting
  • Supports descending with !
  • Includes extended properties
  • Default: DateAdded ascending

Example configuration

30% off least expensive 3 items over $50:

json
{
  "Code": "30OFF",
  "LineItemLevel": true,
  "ItemLimitPerOrder": 3,
  "ItemSortBy": "LineSubtotal",
  "EligibleExpression": "order.Subtotal >= 50",
  "ValueExpression": "item.LineSubtotal * .3"
}

Common scenarios

Order-level discounts

javascript
// $10 off over $50
EligibleExpression: "order.Subtotal > 50"
ValueExpression: "10"

// Free shipping over $60
EligibleExpression: "order.Subtotal >= 60"
ValueExpression: "order.ShippingCost"

Product-specific discounts

javascript
// BOGO (one free)
EligibleExpression: "items.quantity(ProductID = 'ABC') > 1"
ValueExpression: "items.total(ProductID = 'ABC') / items.quantity(ProductID = 'ABC')"

// $5 off with specific product
EligibleExpression: "items.any(ProductID = '123')"
ValueExpression: "5"

Category discounts

javascript
// 15% off category items
EligibleExpression: "item.product.incategory('Bikes')"
ValueExpression: "item.LineSubtotal * .15"

// 10% off sale items (max $20)
EligibleExpression: "items.all(Product.xp.OnSale = true)"
ValueExpression: "min(order.Subtotal * .1, 20)"

Quantity-based discounts

javascript
// 30% off 10+ accessories
EligibleExpression: "items.quantity(product.incategory('GuitarAccessories')) >= 10"
ValueExpression: "items.total(product.incategory('GuitarAccessories')) * .3"

// 20% off product combination
EligibleExpression: "items.any(ProductID = 'ABC') and items.any(ProductID = 'XYZ')"
ValueExpression: "(items.total(ProductID = 'ABC') + items.total(ProductID = 'XYZ')) * .2"

BOGO (scales with quantity)

javascript
EligibleExpression: "items.quantity(ProductID = 'XYZ') > 1"

ValueExpression: "((items.quantity(ProductID='XYZ')/2) - (items.quantity(ProductID='XYZ') % 2 * .5)) * items.total (ProductID='XYZ') / items.quantity(ProductID='XYZ')"

User-based discounts

javascript
// 25% off first order
EligibleExpression: "order.FromUser.xp.FirstOrder = true"
ValueExpression: "order.Subtotal * .25"

// 10% off for registered users
EligibleExpression: "not (order.FromUser.ID = 'myDefaultAnonUserID') and order.Subtotal > 0"
ValueExpression: "order.Subtotal * .10"

Advanced scenarios

javascript
// Multi-tier category discount
EligibleExpression: "items.total(product.incategory('A') >= 10 and item.product.incategory('A')"
ValueExpression: "ifs(items.total(product.incategory('A')) >= 50, item.LineSubtotal .15, items.total(product.incategory('A')) >= 30, item.LineSubtotal * .10, item.LineSubtotal * .05)"

// Order history discount
EligibleExpression: "orderhist.count('1Y') > 4"
ValueExpression: "5"

// Product history discount
EligibleExpression: "itemhist.quantity('6M', 'Product.ID = {productID}') >= 10"
ValueExpression: "order.Subtotal * .10"

Implementation notes

Limitations

  • Expression length: 400 characters
  • Optimize for efficiency
  • Evaluated at application and submit

Requirements

  • Order history requires Premium Search
  • Proper promotion assignments
  • User access via GET v1/me/promotions

Promotion enhancements

OrderCloud Promotions have been enhanced with new properties and endpoints to provide greater flexibility in promotion management and application.

New properties

AutoApply property

Default value: false

This property enables:

  • Automatic promotion application
  • Priority-based application order
  • Controlled promotion sequencing

Active property

Default value: true

This property controls:

  • Promotion evaluation status
  • Buyer user access
  • Historical record keeping

Behavior:

  • Inactive promotions (Active = false):
    • Not evaluated for .../applypromotions
    • Not evaluated for .../eligiblepromotions
    • Not returned from v1/me/promotions
    • Removed from unsubmitted orders upon deactivation
    • Remain accessible via administrative endpoints
    • Available for future reactivation

Priority property

Value type: Nullable

This property enables:

  • Controlled promotion application order
  • Flexible priority values
  • Sequential promotion processing

Implementation notes:

  • No strict sequencing rules
  • Used by applypromotions and refreshpromotions
  • Determines evaluation order

New endpoints

Eligible promotions endpoints

v1/orders/{direction}/{orderID}/eligiblepromotions and v1/cart/eligiblepromotions

These endpoints support:

  • Listing eligible promotions for orders
  • Displaying promo codes during checkout
  • Enhanced promotion-based UI features

Refresh promotions endpoints

v1/orders/{direction}/{orderID}/refreshpromotions and v1/cart/refreshpromotions

These endpoints handle:

Automatic promotion application
  • Applies eligible AutoApply promotions
  • Sorts by Priority value
  • Processes null Priority values last
  • Limits to 100 promotions per API call
Promotion removal
  • Removes ineligible applied promotions
  • Handles CanCombine conflicts
  • Processes Priority-based removals
Promotion recalculation
  • Recalculates existing promotion discounts
  • Updates order totals
  • Maintains promotion integrity
Response details

Returns arrays of:

  • PromosAdded: Newly applied promotions
  • PromosRemoved: Removed promotions

Note: Prefer /refreshpromotions over /applypromotions for most use cases.

Apply promotions endpoints

v1/orders/{direction}/{orderID}/applypromotions and v1/cart/applypromotions

These endpoints provide:

Automatic application
  • Processes eligible AutoApply promotions
  • Sorts by Priority value
  • Handles null Priority values last
  • Limits to 100 promotions per call
Response format

Returns the complete Order object with:

  • Applied promotions
  • Updated totals
  • Order details

Line item-level promotions

Promotions have become one of OrderCloud's more popular features, largely due to flexibility of rules-based expressions in determining both the eligibility and value of a promotion relative to an order. Today we're taking this capability a step farther by introducing the ability to associate a promotion directly with one or more line items on an order.

Benefits

Currently, all promotional discounts "live" at the order level. Expressions can make certain item-level assertions (e.g. items.any(...)) and calculations (e.g. items.total(...)), so you may be able to infer which line items triggered which discounts by referring back to the rule expressions, but we've heard multiple requests to break out discount amounts by the items they apply to. A couple use cases cited: making it easier to calculate item-level tax and partial returns. Who wants to reverse-engineer the rules engine to figure these out?

Implementation

To remove any ambiguity (a central theme of this feature), you must first declare whether a promotion applies at the order level or line item level. Do this by setting Promotion.LineItemLevel to true or false. There's also 2 new read-only fields on the LineItem model: LineSubtotal and PromotionDiscount. The existing LineTotal field is simply the former minus the latter. (Order.PromotionDiscount still reflects the total of all item-level and order-level discounts, not just the latter.) Also, the models you get back in a call to GET v1/orders/{direction}/{id}/promotions will contain a LineItemID field, which will be null in the case of order-level promos. Together, these enhancements should give you what you need to easily and unambiguously associate specific promotions and discount amounts with specific line items.

Expression syntax

The existing items functions aren't quite enough to determine which item(s) to associate with a promo. Simple example: buy product X and get product Y for free. You can imagine writing your EligibleExpression with a pair of items.any, and it'll work, but we won't know which of the 2 items to tie the discount to.

Enter the new item (singular) token. When used in an EligibleExpression, think of it as sort of a selector of the line item (or multiple line items) that you want the promo applied to. Any property of the full-blown LineItem model is valid on item in an expression, as is the incategory function. In the example above, the EligibleExpression would be:

item.ProductID = 'Y' and items.any(ProductID = 'X')

Now the discount will be tied unambiguously to lines item(s) containing product Y, fulfilling the original intent. As you can see, items is still allowed in this context, but it's used to assert "other" parts of the order, not to identify the item(s) to tie the promo to. You could do order-level assertions here too, e.g. order.Total > 100.

For line item-level promos, item is required in EligibleExpression and optional in ValueExpression. For order-level promos, it's not allowed for either.

Kudos and bonus points if you already thought it through this far, but let's take our example over the finish line by specifying the ValueExpression. Here we can use our item token, which has already "selected" product Y, and just specify the price of one of them. Easy enough:

item.UnitPrice
If you have suggestions for improving this article, let us know!