Skip to main content

Guide: Reading from GET · ← Application

rule_effects — complete reference

:::info Field in the GET response In GET /points and GET /points/{id}, this block is returned in **rule_effects** (JSON string). Validate rule_group for bundles (not legacy variants such as rule_grouped in older samples). :::

Defines what benefit the campaign grants (fixed points, ratio, percentage on lines/total, etc.) and how it is split between the ticket and lines.

Question it answers: “If the campaign won, how many points/balance does it grant and on what cart or event basis?”

On this page​


Effects and distributions matrix by when​

whenAllowed effectsDistributionsrule_group / limits
after_create_activity (cart)percentage, ratio, direct_value, strikethrough, combo_price, formulaAll three (total, individual_items, individual_items_pro_rata) with per-type rules — see table belowYes for percentage, direct_value, combo_price
Trigger slugdirect_value onlytotal onlyNo (rule_group: false)
after_create_custom_entitypercentage, ratio, direct_valuetotal onlyNo
manualdirect_value onlytotal onlyNo

Distribution restrictions by effect type (cart only; for trigger, custom event, and manual use only the matrix row above):

Effecttotalindividual_itemsindividual_items_pro_rata
percentage✓✓✓
direct_value✓✓✓
ratio✓✓✓
strikethrough—✓ (only)—
combo_price✓—✓
formula—✓ (only)—

For trigger, custom event, and manual: distribution is always total, no bundle (rule_group: false). Effect limits (limits) and bundle mechanics apply on cart only.

Monetary value (points and wallet)​

:::warning Required reading for integrations In points/wallet campaigns, rule_effects.value is always persisted as a monetary amount in the database, regardless of accrual_mode (points or money). Do not read value as “number of points” unless you have converted from money. :::

Requirements tied to each campaign:

RequirementPurpose
Campaign currencyCurrency in which monetary value is expressed.
score_exchange for that currency (Company configuration)How many monetary units equal 1 point (points ↔ money exchange rate). Each currency typically has one score_exchange.

Convert to points (when accrual_mode is points, or when you must show units to the customer):

points_granted = monetary_value / score_exchange

Example: rule “1 point every €10” with each point worth €0.01:

Business meaningValue in GET / DB (rule_effects.value)
1 point = €0.010.01 (campaign currency, e.g. EUR)
Ratio “every €10 → 1 point”In ratio, unit: 10 and value: 0.01 (not value: 1 in points)

If the integration displays points to the customer, divide (or apply the API-agreed formula) using the active score_exchange for currency. With accrual_mode: money, wallet balance usually aligns 1:1 with the monetary amount accrued; still, effect configuration stores value in money.

On ticket scores[], the engine returns calculated accrual; when debugging mismatches, always start from monetary GET value and score_exchange, not a literal point count in rule_effects.


Here **rule_group** (bundle grouping in the effect) does matter for integrations — unlike bundle / grouped in rule_conditions, which are internal and ignored.

Common fields in GET​

FieldRole
key / typeIdentify the effect; usually match (percentage, direct_value, …)
valueMain magnitude: for points/wallet, monetary amount (direct_value, ratio base) or percentage (percentage). See monetary value. Does not apply to strikethrough or formula
unitOnly **ratio**: monetary reference unit (e.g. every €10)
distributionHow the effect is split (see per-type table below)
limitsMaximum times the effect can apply in the same transaction
rule_grouptrue = grouping / bundle (only percentage, direct_value, combo_price). Requires quantities in conditions + application — see Bundle mechanics
formulaOnly **formula** effect: fixed calculation expression
strikethrough_dataOnly **strikethrough**: map product key → promotional price. Key may be **product_id** or **product_code** depending on the Company (must match the line identifier on the ticket)

limits (effect limit, not win limit)​

limits in rule_effects caps how many times the effect can apply in the same transaction (same ticket). It is not the campaign win cap (once, budget, etc.) nor a unit cap in rule_conditions.

ValueInterpretation
0No limit: the effect may apply as many times as conditions and application allow
> 0At most that many effect applications on the ticket

What counts as one application depends on distribution and rule_group:

ConfigurationWhat counts as 1 applicationWhat limits: N caps
distribution: individual_items and rule_group null / falseOne unit (internal line with qty 1; see expansion below)Maximum units that get the benefit on the ticket
rule_group: true (bundle / 2x1 / combo)One group built per quantity in rule_conditions + rule_applicationMaximum groups that receive the benefit (e.g. how many 2x1 deals)

One unit = one line in the engine (even when the ticket has quantity > 1)​

For effect calculation, Loyal Guru treats each unit as a separate line. If the integration sends a single line with quantity: 10, the engine splits it internally into 10 lines of quantity 1, runs conditions, rule_application, effects, and limits on that unit model, then merges the result back into the same lines received on the ticket (same number of lines and quantities the retailer sent).

Implications for integrations:

  • With distribution: individual_items, limits counts units (internal qty-1 lines), not necessarily rows in the lines[] JSON. limits: 10 with one aggregated line of qty 15 → at most 10 units get the accrual on that line (the remaining 5 stay at full price for that effect).
  • On POST /activities, scores[] may show accrual consolidated per original line; internal logic still used the unit model.
  • Win conditions and application are also evaluated on expanded units when the engine requires it. Expansion is transparent when you only read the campaign GET, but it matters when interpreting limits and when debugging multi-quantity lines.

Example: line-level % with a unit cap (no bundle)​

Goal: 5% on selected products, on at most 10 units on the ticket.

FieldValueWhy
typepercentagePercentage accrual
value55%
distributionindividual_itemsThe % is calculated per unit (internal qty-1 line)
rule_groupnull or falseNo grouping: each application = one unit
limits10At most 10 units get the 5% on that purchase

rule_application must scope products (e.g. product_id or product_categories on lines). If the ticket has one line with quantity: 15 of the eligible product, the engine treats it as 15 units; with limits: 10 only 10 get the 5% (usually reflected on that same aggregated line in the response). With several rows in lines[], the cap of 10 is shared across units from all of them per priority/stacking.

{
"type": "percentage",
"value": 5,
"distribution": "individual_items",
"rule_group": null,
"limits": 10
}

Example: 2x1 with a group cap (rule_group)​

Goal: 2x1 mechanics but only two 2x1 deals per ticket (two bonified groups).

FieldValueWhy
rule_grouptrueEach application = one group (2+1 pair), not a standalone line
limits2At most 2 groups with benefit in the transaction

Win conditions (rule_conditions) define group size (e.g. quantity ≥ 2); rule_application defines how many units in the group get the benefit (e.g. ≥ 1). Full walkthrough: Bundle mechanics (rule_group) — same ticket with 8 units and limits: 2 only bonifies 2 of 4 possible groups.

Do not confuse with win limits (once, num_per_day, budget, etc.) in the campaign’s general configuration.

Allowed distributions by effect type​

Effecttotalindividual_itemsindividual_items_pro_rata
percentage✓✓✓
direct_value✓✓✓
ratio✓✓✓
strikethrough—✓ (only)—
combo_price✓—✓
formula—✓ (only)—

Distribution restrictions by when​

whenAllowed distributions
CartPer effect type (table above)
Trigger, custom event, manualtotal only
DistributionBrief behavior
totalA single accrual on the qualified set / transaction total
individual_itemsThe effect is evaluated per unit (each unit as an internal qty-1 line; see unit expansion)
individual_items_pro_rataThe benefit is split proportionally across qualified lines

Stacking (stackability) by effect​

EffectCart mode
strikethroughAlways Highest Discount
combo_priceAlways Highest Discount
percentage, ratio, direct_value, formulaStackable or Highest Discount (configurable)

Details: Points Campaigns.


percentage​

:::info Availability Cart event only (after_create_activity). Not available for trigger, custom event, or manual — see matrix by when. :::

Percentage accrual on qualified lines or total (monetary balance from % of the base; see monetary value).

{
"key": "percentage",
"type": "percentage",
"value": 20,
"limits": 0,
"formula": null,
"rule_group": null,
"distribution": "individual_items"
}
FieldReading
value: 2020% accrual
distribution: individual_itemsThe % applies on each line that meets application
limits: 0No cap on bonified units; with limits: 10, at most 10 units — see % cap example
rule_group: nullNo grouping (e.g. simple % on a category). With true, activates groups per quantity in conditions and application

Integration: if the accrual does not appear on the expected ticket lines, check **rule_application** in addition to the % — for cart campaigns, application is usually required for line-level % (Operational details §5).


direct_value​

:::info Availability Cart: yes. Trigger, custom event, manual: this type only, with distribution: total. :::

Fixed accrual amount, expressed in the campaign currency in value (see monetary value).

{
"key": "direct_value",
"type": "direct_value",
"value": 5,
"limits": 0,
"formula": null,
"rule_group": null,
"distribution": "individual_items"
}
FieldReading
value: 5€5 (or other currency) of monetary balance per currency; in points mode, convert to units with score_exchange
distribution: individual_items€5 per qualified line; with total, a single €5 block on the set
rule_groupSame as percentage: true for bundles that split a fixed amount per group

Common case: €5 accrual on basket → direct_value, distribution: total, condition total_cart >= X.


ratio​

:::info Availability Cart and custom event only. Not on trigger or manual. :::

Accrual proportional to spend: for each unit (amount in campaign currency) on the qualified base, value in money (not whole points) is granted, split per distribution.

{
"key": "ratio",
"type": "ratio",
"unit": 10,
"value": 0.01,
"limits": 2,
"formula": null,
"distribution": "individual_items_pro_rata"
}
FieldReading of the example
unit: 10, value: 0.01Every €10 qualified → €0.01 balance (= 1 point if score_exchange is 0.01)
distribution: individual_items_pro_rataThat benefit is prorated across participating lines
limits: 2At most 2 applications of the ratio in the same transaction (e.g. up to €20 base → 2 × 1 benefit if the rule allows)

value and unit in ratio are in currency. With accrual_mode: points, convert the result to points using score_exchange before showing it to the customer.


strikethrough​

:::info Availability Cart only. Accrues balance equivalent to ticket price minus promotional price (same mechanics as strikethrough discount, expressed as accrual). :::

Replaces the line price on the ticket with the promotional price in strikethrough_data. The applied accrual is the difference between the price sent on the ticket and the effect’s promotional price.

{
"key": "strikethrough",
"type": "strikethrough",
"limits": 0,
"formula": null,
"distribution": "individual_items",
"strikethrough_data": {
"233212": 1,
"343434": 9
}
}
FieldReading
strikethrough_dataMap { "product_key": promotional_price }. Key is **product_id** or **product_code** as the Company uses in catalog/ticket — must be the same identifier type the integration sends on the line
No valueDiscount amount comes from per-line calculation (ticket price − promo price)
distributionAlways individual_items (one evaluation per line / product)
limitsSame semantics (0 = no cap on effect applications)

Map keys: within one campaign, entries are usually homogeneous (all numeric ID or all code). If the Company uses SKU, expect keys like "12000041"; if internal ID, keys like "233212".

Numeric example (by product_id): strikethrough_data["233212"]: 1, line on ticket with that product and price 3 → effective price 1, accrual 2 (3 − 1).

Example (by product_code): strikethrough_data["CC-33CL-LATA"]: 0.99 — the ticket line must carry the same product_code to match.

Integration: if it does not apply, verify key type (ID vs code) aligned with the ticket, that the key exists in the map, and the line price sent. Stacking: always Highest Discount.


combo_price​

:::info Availability Cart only. Accrues balance from the bundle’s fixed price (shared mechanics with discount combo). :::

Fixed bundle price for a product group that meets conditions / application.

{
"key": "combo_price",
"type": "combo_price",
"value": 10.9,
"limits": 0,
"formula": null,
"rule_group": true,
"distribution": "individual_items_pro_rata"
}
FieldReading
value: 10.9Total combo price (€10.9)
rule_group: trueEngine groups lines that meet bundle mechanics before setting combo price
distribution: individual_items_pro_rataCombo savings are split across group lines. Alternative: total (single block on the set)
limits: 0No cap on how many times the combo can apply on the ticket

Restriction: do not use individual_items with combo_price — only total or individual_items_pro_rata. Stacking: always Highest Discount.


formula (external promotional effect)​

:::info Availability Cart only. Per-line balance from promoPrice (or agreed attribute) sent by external integration. :::

Per-line accrual calculated from an attribute sent by an external system (precomputed promo price).

{
"key": "formula",
"type": "formula",
"limits": 0,
"formula": "lines.total - lines.attributes.promoPrice",
"distribution": "individual_items"
}
FieldReading
formulaFixed in configuration: lines.total - lines.attributes.promoPrice
No valueAmount comes from per-line calculation
distributionAlways individual_items
rule_groupDoes not use grouping (formula does not support bundle)

Integration flow: the integrator must send promoPrice (or agreed equivalent) in ticket line attributes. If missing on a line, that line gets no accrual from this campaign. If the result ≤ 0, no accrual on that line.


Bundle mechanics (rule_group)​

In Owner App, Bundle / group (rule_group: true) tells the engine to form groups of lines and apply the effect per group, not independently line by line.

Where it applies​

rule_effects.rule_groupCompatible effect types
truepercentage, direct_value, combo_price
false / nullAny effect without grouping
Not availableratio, strikethrough, formula

The three blocks that define the bundle​

BlockQuestion it answersWhat to validate in integration
**rule_conditions**How many units are needed to form a group?Same product/category clause + quantity subcondition (e.g. gte 2 → groups of 2)
**rule_application**On how many units in the group does the effect apply?Same product/category family + quantity subcondition (e.g. gte 1 → 1 unit with benefit per group)
**rule_effects**What benefit and how many times?rule_group: true, value / type, distribution, **limits**

The bundle and grouped flags in conditions/application JSON are internal (ignore value). What matters operationally are nested **quantity** and **rule_group** on the effect.

Full example: 2x1 on product 2222 (100% on 1 of every 2)​

Win — form groups of 2 units (rule_conditions):

{
"conditions": {
"and": [
{
"key": "cart_product_id",
"field": "product_code",
"bundle": 1,
"values": ["2222"],
"grouped": true,
"operator": "in",
"conditions": [
{ "field": "quantity", "value": 2, "operator": "gte" }
],
"array_field": "lines"
}
]
}
}

Application — accrual on 1 unit per group (rule_application):

{
"conditions": {
"and": [
{
"key": "cart_product_id",
"field": "product_code",
"bundle": 1,
"values": ["2222"],
"grouped": true,
"operator": "in",
"conditions": [
{ "field": "quantity", "value": 1, "operator": "gte" }
],
"array_field": "lines"
}
]
}
}

Effect — 100% on that unit, grouping on (rule_effects):

{
"key": "percentage",
"type": "percentage",
"value": 100,
"limits": 1,
"formula": null,
"rule_group": true,
"distribution": "individual_items_pro_rata"
}

2x1 interpretation​

StepReading
Conditions quantity: 2Each valid group = 2 units of product 2222 on the ticket
Application quantity: 1Within each group, 100% applies to 1 unit (the “free” or bonified unit in 2x1)
rule_group: trueEngine groups before calculating; without this, % would be read line by line without 2x1 mechanics
distribution: individual_items_pro_rataSplit of the bonified unit’s accrual across group lines (typical in 2x1)
Ticket with 8 units of 22228 ÷ 2 = 4 groups → up to 4 effect applications (4 bonified units if no cap)

Effect limits with bundles​

With rule_group: true, each effect application is one group (not a standalone line). So limits: 2 on a 2x1 means “at most two 2x1 deals on the ticket”, not “two units total”. See also 2x1 group cap example.

limitsIn the 2x1 example
0No cap: all possible groups on the ticket get benefit (8 units → 4 groups → 4 bonifications)
1Only 1 group gets the effect in the transaction (1 “free” product even if more groups are possible)
2At most 2 groups (two 2x1 deals); with 8 units on ticket only 2 of 4 possible groups are bonified

**limits** counts effect applications per transaction (bonified groups with rule_group, or lines with individual_items without grouping), not total units in the cart.

Visual summary​

Ticket: 8 × product 2222
│
rule_conditions (qty ≥ 2) → 4 groups of 2
│
rule_application (qty ≥ 1) → 1 unit with accrual per group
│
rule_effects (100%, rule_group) + limits
│
limits: 0 → 4 bonifications (4 groups)
limits: 2 → 2 bonifications (only 2 of 4 groups)

Common variants​

MechanicsConditions (group)Application (benefit)Typical effect
2x1 same productquantity gte 2 on productquantity gte 1 on same productpercentage: 100 + rule_group: true
3x2 on categoryquantity gte 3 on categoryquantity gte 1 on categorypercentage: 100 + rule_group: true
Bundle A + B + C products for €10quantity gte 1 on A + quantity gte 1 on B + quantity gte 1 on Cbundle line filtercombo_price + rule_group: true
Fixed € per groupquantity gte Nquantity gte 1direct_value + rule_group: true

Common mistakes — bundle​

SymptomUsual cause
2x1 does not workrule_group: false or missing quantity subcondition in conditions or application
Over-bonifieslimits: 0 with many groups on ticket; raise limits to cap bonified groups
Under-bonifiesLow limits (e.g. 1) with many units; or conditions require more units than on the ticket
Different product in applicationvalues / field in application do not match win conditions
Confusing bundle/grouped flagsThey do not enable mechanics alone; need **rule_group: true** and correct **quantity** values

Configuration vs ticket response​

ContextFieldContents
Campaign in GETrule_effects (often JSON string)Configuration: type, value, distribution, limits, …
Evaluated ticket / purchasescores[] (primary)Calculated points/balance per campaign or line
Discounts on same ticketeffects[]Monetary discounts on the same activity

Typical fields in ticket scores[]:

Ticket fieldRelation to config
rule_id / campaign_idPoints campaign ID
points / amountPoints or balance granted (per API contract)
lineTicket line when distribution is per item
status / datesMay reflect activation/expiration per integration

Example (ratio on cart):

{
"scores": [{
"rule_id": 201,
"line": 1,
"points": 150,
"currency": "POINTS"
}]
}

Integration: compare GET rule_effects with scores[] on POST /activities (or equivalent). If scores[] is empty, check when, eligibility (rule_conditions, audience), rule_application, company apply_points, and activation/expiration metadata — not coupon or POS fields.

Activation and expiration (GET metadata)​

Besides rule_effects, GET /points usually includes balance lifecycle fields:

FieldUse
activation_period_type + activation_delayWhen balance moves from pending to available
expiration_period_type + availability_durationWhen balance expires
currencyPoints or wallet currency

These explain differences between GET configuration and balance visible in the customer app/wallet.


Common mistakes — rule_effects​

ConfusionHow to resolve it
limits: 0 “disables limits”Correct: no cap on effect applications in the transaction
One line with quantity: 20 and limits: 10Engine expands to 20 units; only 10 get the effect; response may still be one line with aggregated accrual — see unit expansion
limits vs win limitsrule_effects.limits = effect repetitions; once / num_* = how many times the customer can win the campaign
Bundle / 2x1Trio: quantity in rule_conditions (group size) + quantity in rule_application (units with accrual) + rule_group: true + limits
strikethrough “did not accrual as expected”Map key (product_id vs product_code) must match line identifier on ticket; accrual = ticket price − promo price
combo_price with individual_itemsInvalid in product; use total or individual_items_pro_rata
formula without promoPrice on lineIncomplete integration; accrual 0 on that line
% or ratio without application on cartCheck rule_application — usually required for line-level %
Same % in config, different on ticketCheck stackability (Highest Discount won another campaign) and effects[]


Back: Reading from GET · Checklist