The future is agentic.

Tutorials

Close the promotion gap with intelligent upselling.

Implement proactive upselling by simulating promotions with the Rules Engine, injecting candidate SKUs into the cart payload, and surfacing contextual offers in real time.

SU
Sebastián
· March 12, 2026
The problem

Understanding the promotion matching gap

Imagine you have products included in a promotion, but they're not performing as expected. Is the product itself not appealing? Is the pricing wrong? Or are customers simply unable to identify the right product combinations needed to trigger the promotion?

If the first two aren't the issue, this article is for you.

Flex promotions are automatically applied when a specific set of conditions is met — either within a cart or through a coupon. But there's a gap: a customer may be just one product away from qualifying for a promotion, with no way for them to know it.

The /api/rules/check endpoint closes that gap. It lets you simulate whether a set of rules would match a given cart — without actually applying any discounts. Think of it as a dry-run evaluator: you inject hypothetical products into the cart payload, run it against your promotion rules, and if there's a match, you surface a targeted suggestion in the UI.

The plan

The upsell evaluation flow

The overall flow looks like this:

Step 1

Fetching the promotions

First, retrieve all the flex promotions tagged as upsell:

GET /api/flex_promotions?filter[q][tags_name_eq]=upsell

For each promotion returned, fetch its full rules payload as follows (where {id} is the ID of the single promotion):

GET /api/flex_promotions/{id}

Store the rules in an array. For this example, we'll work with two promotions:

[
  {
    "id": "p0",
    "name": "Get 50% off if product has tag 'on-sale'",
    "conditions": [
      {
        "field": "order.line_items.sku.tags.id",
        "matcher": "array_match",
        "value": {
          "in_or": ["mJPnBfEezQ"]
        },
        "group": "discountable_items"
      }
    ],
    "actions": [
      {
        "type": "percentage",
        "value": 0.5,
        "selector": "order.line_items",
        "groups": ["discountable_items"]
      }
    ]
  },
  {
    "id": "p1",
    "name": "Get 25% off if product is in the summer collection SKU list",
    "conditions": [
      {
        "field": "order.line_items.sku.sku_list_items.sku_list.id",
        "matcher": "eq",
        "value": "BbpjSNNkAZ",
        "group": "discountable_items"
      }
    ],
    "actions": [
      {
        "type": "percentage",
        "value": 0.25,
        "selector": "order.line_items",
        "groups": ["discountable_items"]
      }
    ]
  }
]

Find here below the ID reference for this tutorial:

ID

Resource

mJPnBfEezQ

Tag on-sale

BbpjSNNkAZ

SKU list summer collection

DGjtgmdLjW

Candidate SKU A

WaDeSyykMZ

Candidate SKU B

oKkhYLlzgE

Customer's current order

gnYtPpKLeG

Existing line item in the order

Step 2

Fetching the canditate products

Now you need to retrieve all the SKUs tagged as upsell, including their tags and SKU lists (both are required because the rules from Step 1 reference them):

GET /api/skus?filter[q][tags_name_eq]=upsell&include=tags,sku_lists

From the response, build a candidate array in the following format (normalized to fit the payload structure required by the check endpoint):

[
  {
    "id": "DGjtgmdLjW",
    "tags": [
      { "type": "tags", "id": "mJPnBfEezQ" }
    ],
    "sku_list_items": [
      { "sku_list": { "id": "BbpjSNNkAZ" } }
    ]
  },
  {
    "id": "WaDeSyykMZ",
    "tags": [
      { "type": "tags", "id": "BJgxNfjoqN" }
    ],
    "sku_list_items": [
      { "sku_list": { "id": "BbpjSNNkAZ" } }
    ]
  }
]
Step 3

Calling the check endpoint

The /check endpoint accepts the following request body:

{ 
  "payload": [
	  { ... }
	], 
	"rules": [
	  { ... }
	] 
}

Where:

  • payload — represents the context the rules are evaluated against (in this case, the customer's current cart).
  • rules — is the rules array extracted in Step 1.

How the loop works

For each rule in your array (Step 1), build the cart payload by injecting all candidate products (Step 2) as additional line items. Set quantity: 1 for each of them (you can adjust this value based on your business logic).

Here is the complete request body for the first iteration (rule p0):

{
  "payload": [
    {
      "order": {
        "id": "oKkhYLlzgE",
        "line_items": [
          {
            "quantity": 1,
            "id": "gnYtPpKLeG",
            "sku": {
              "id": "anZtGpkLeP",
              "code": "SKU0"
            }
          },
          {
            "quantity": 1,
            "sku": {
              "id": "DGjtgmdLjW",
              "tags": [
                { 
								  "type": "tags",
									"id": "mJPnBfEezQ" 
								}
              ],
              "sku_list_items": [
                { 
								  "sku_list": { "id": "BbpjSNNkAZ" }
								}
              ]
            }
          }
        ]
      }
    }
  ],
  "rules": [
    {
      "id": "p0",
      "name": "Get 50% off if product has tag 'on-sale'",
      "conditions": [
        {
          "field": "order.line_items.sku.tags.id",
          "matcher": "array_match",
          "value": { 
					  "in_or": ["mJPnBfEezQ"] 
					},
          "group": "discountable_items"
        }
      ],
      "actions": [
        {
          "type": "percentage",
          "value": 0.5,
          "selector": "order.line_items",
          "groups": [ "discountable_items" ]
        }
      ]
    }
  ]
}

The response will look like this:

{
  "data": [
    {
      "id": "p0",
      "name": "Get 50% off if product has tag 'on-sale'",
      "priority": 0,
      "match": true,
      "conditions_logic": "and",
      "conditions": [
        {
          "field": "order.line_items.sku.tags.id",
          "matcher": "array_match",
          "value": { "in_or": ["mJPnBfEezQ"] },
          "group": "discountable_items",
          "match": true,
          "matches": [
            {
              "order": "oKkhYLlzgE",
              "line": "items",
              "sku": "DGjtgmdLjW",
              "group": "discountable_items"
            }
          ],
          "scope": "any"
        }
      ],
      "actions": [
        {
          "resources": [
            {
              "resource_type": "line_items",
              "id": "gnYtPpKLeG",
              "group": "discountable_items",
              "quantity": 1,
              "value": 0.5,
              "action_type": "percentage"
            }
          ]
        }
      ]
    }
  ]
}

Repeat the loop for each rule in the array from Step 1.

What the customer sees

Once you've identified rules with that match you candidate products, your frontend has everything it needs to display a contextual suggestion in the cart:

You already know which product to suggest (the SKU from the array in Step 2 that triggered the match), and you already know the discount that will be applied (from the rule's action). No additional API calls are needed.

Advanced possibilities

This is a minimal example with one rule and two candidate SKUs, but the pattern can be scaled easily:

  • Multiple products and multiple rules
    Iterate over all combinations and select the most advantageous promotion to surface.
  • Prioritization
    If multiple rules match, show the one with the highest discount or the closest expiry date
  • Customer personalization
    Combine the upsell tag with customer-level conditions (e.g. segment, order history) for targeted suggestions.
  • A/B testing
    Use the check endpoint to simulate different promotional strategies without touching the live cart.

Try it out

If you want to start experimenting with advanced promotion logic, request a free trial period directly from your Dashboard in the Promotions app by clicking on Flex promotion. We'll help you get started with the Rules Engine and tailor it to your business needs.