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 - Monetary
value(points and wallet) - Common fields in GET
limits(effect limit, not win limit)- Allowed distributions by effect type
- Distribution restrictions by
when - Stacking (stackability) by effect
percentagedirect_valueratiostrikethroughcombo_priceformula(external promotional effect)- Bundle mechanics (
rule_group) - Configuration vs ticket response
- Activation and expiration (GET metadata)
- Common mistakes —
rule_effects
Effects and distributions matrix by when
when | Allowed effects | Distributions | rule_group / limits |
|---|---|---|---|
after_create_activity (cart) | percentage, ratio, direct_value, strikethrough, combo_price, formula | All three (total, individual_items, individual_items_pro_rata) with per-type rules — see table below | Yes for percentage, direct_value, combo_price |
| Trigger slug | direct_value only | total only | No (rule_group: false) |
after_create_custom_entity | percentage, ratio, direct_value | total only | No |
manual | direct_value only | total only | No |
Distribution restrictions by effect type (cart only; for trigger, custom event, and manual use only the matrix row above):
| Effect | total | individual_items | individual_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:
| Requirement | Purpose |
|---|---|
Campaign currency | Currency 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 meaning | Value in GET / DB (rule_effects.value) |
|---|---|
| 1 point = €0.01 | 0.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
| Field | Role |
|---|---|
key / type | Identify the effect; usually match (percentage, direct_value, …) |
value | Main magnitude: for points/wallet, monetary amount (direct_value, ratio base) or percentage (percentage). See monetary value. Does not apply to strikethrough or formula |
unit | Only **ratio**: monetary reference unit (e.g. every €10) |
distribution | How the effect is split (see per-type table below) |
limits | Maximum times the effect can apply in the same transaction |
rule_group | true = grouping / bundle (only percentage, direct_value, combo_price). Requires quantities in conditions + application — see Bundle mechanics |
formula | Only **formula** effect: fixed calculation expression |
strikethrough_data | Only **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.
| Value | Interpretation |
|---|---|
0 | No limit: the effect may apply as many times as conditions and application allow |
> 0 | At most that many effect applications on the ticket |
What counts as one application depends on distribution and rule_group:
| Configuration | What counts as 1 application | What limits: N caps |
|---|---|---|
distribution: individual_items and rule_group null / false | One 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_application | Maximum 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,limitscounts units (internal qty-1 lines), not necessarily rows in thelines[]JSON.limits: 10with 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
limitsand 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.
| Field | Value | Why |
|---|---|---|
type | percentage | Percentage accrual |
value | 5 | 5% |
distribution | individual_items | The % is calculated per unit (internal qty-1 line) |
rule_group | null or false | No grouping: each application = one unit |
limits | 10 | At 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).
| Field | Value | Why |
|---|---|---|
rule_group | true | Each application = one group (2+1 pair), not a standalone line |
limits | 2 | At 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
| Effect | total | individual_items | individual_items_pro_rata |
|---|---|---|---|
percentage | ✓ | ✓ | ✓ |
direct_value | ✓ | ✓ | ✓ |
ratio | ✓ | ✓ | ✓ |
strikethrough | — | ✓ (only) | — |
combo_price | ✓ | — | ✓ |
formula | — | ✓ (only) | — |
Distribution restrictions by when
when | Allowed distributions |
|---|---|
| Cart | Per effect type (table above) |
| Trigger, custom event, manual | total only |
| Distribution | Brief behavior |
|---|---|
total | A single accrual on the qualified set / transaction total |
individual_items | The effect is evaluated per unit (each unit as an internal qty-1 line; see unit expansion) |
individual_items_pro_rata | The benefit is split proportionally across qualified lines |
Stacking (stackability) by effect
| Effect | Cart mode |
|---|---|
strikethrough | Always Highest Discount |
combo_price | Always Highest Discount |
percentage, ratio, direct_value, formula | Stackable 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"
}
| Field | Reading |
|---|---|
value: 20 | 20% accrual |
distribution: individual_items | The % applies on each line that meets application |
limits: 0 | No cap on bonified units; with limits: 10, at most 10 units — see % cap example |
rule_group: null | No 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"
}
| Field | Reading |
|---|---|
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_group | Same 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"
}
| Field | Reading of the example |
|---|---|
unit: 10, value: 0.01 | Every €10 qualified → €0.01 balance (= 1 point if score_exchange is 0.01) |
distribution: individual_items_pro_rata | That benefit is prorated across participating lines |
limits: 2 | At most 2 applications of the ratio in the same transaction (e.g. up to €20 base → 2 × 1 benefit if the rule allows) |
valueandunitinratioare in currency. Withaccrual_mode: points, convert the result to points usingscore_exchangebefore 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
}
}
| Field | Reading |
|---|---|
strikethrough_data | Map { "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 value | Discount amount comes from per-line calculation (ticket price − promo price) |
distribution | Always individual_items (one evaluation per line / product) |
limits | Same 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"
}
| Field | Reading |
|---|---|
value: 10.9 | Total combo price (€10.9) |
rule_group: true | Engine groups lines that meet bundle mechanics before setting combo price |
distribution: individual_items_pro_rata | Combo savings are split across group lines. Alternative: total (single block on the set) |
limits: 0 | No 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"
}
| Field | Reading |
|---|---|
formula | Fixed in configuration: lines.total - lines.attributes.promoPrice |
No value | Amount comes from per-line calculation |
distribution | Always individual_items |
rule_group | Does 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_group | Compatible effect types |
|---|---|
true | percentage, direct_value, combo_price |
false / null | Any effect without grouping |
| Not available | ratio, strikethrough, formula |
The three blocks that define the bundle
| Block | Question it answers | What 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
| Step | Reading |
|---|---|
Conditions quantity: 2 | Each valid group = 2 units of product 2222 on the ticket |
Application quantity: 1 | Within each group, 100% applies to 1 unit (the “free” or bonified unit in 2x1) |
rule_group: true | Engine groups before calculating; without this, % would be read line by line without 2x1 mechanics |
distribution: individual_items_pro_rata | Split of the bonified unit’s accrual across group lines (typical in 2x1) |
Ticket with 8 units of 2222 | 8 ÷ 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.
limits | In the 2x1 example |
|---|---|
0 | No cap: all possible groups on the ticket get benefit (8 units → 4 groups → 4 bonifications) |
1 | Only 1 group gets the effect in the transaction (1 “free” product even if more groups are possible) |
2 | At 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 withrule_group, or lines withindividual_itemswithout 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
| Mechanics | Conditions (group) | Application (benefit) | Typical effect |
|---|---|---|---|
| 2x1 same product | quantity gte 2 on product | quantity gte 1 on same product | percentage: 100 + rule_group: true |
| 3x2 on category | quantity gte 3 on category | quantity gte 1 on category | percentage: 100 + rule_group: true |
| Bundle A + B + C products for €10 | quantity gte 1 on A + quantity gte 1 on B + quantity gte 1 on C | bundle line filter | combo_price + rule_group: true |
| Fixed € per group | quantity gte N | quantity gte 1 | direct_value + rule_group: true |
Common mistakes — bundle
| Symptom | Usual cause |
|---|---|
| 2x1 does not work | rule_group: false or missing quantity subcondition in conditions or application |
| Over-bonifies | limits: 0 with many groups on ticket; raise limits to cap bonified groups |
| Under-bonifies | Low limits (e.g. 1) with many units; or conditions require more units than on the ticket |
| Different product in application | values / field in application do not match win conditions |
Confusing bundle/grouped flags | They do not enable mechanics alone; need **rule_group: true** and correct **quantity** values |
Configuration vs ticket response
| Context | Field | Contents |
|---|---|---|
| Campaign in GET | rule_effects (often JSON string) | Configuration: type, value, distribution, limits, … |
| Evaluated ticket / purchase | scores[] (primary) | Calculated points/balance per campaign or line |
| Discounts on same ticket | effects[] | Monetary discounts on the same activity |
Typical fields in ticket scores[]:
| Ticket field | Relation to config |
|---|---|
rule_id / campaign_id | Points campaign ID |
points / amount | Points or balance granted (per API contract) |
line | Ticket line when distribution is per item |
status / dates | May 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:
| Field | Use |
|---|---|
activation_period_type + activation_delay | When balance moves from pending to available |
expiration_period_type + availability_duration | When balance expires |
currency | Points or wallet currency |
These explain differences between GET configuration and balance visible in the customer app/wallet.
Common mistakes — rule_effects
| Confusion | How to resolve it |
|---|---|
limits: 0 “disables limits” | Correct: no cap on effect applications in the transaction |
One line with quantity: 20 and limits: 10 | Engine expands to 20 units; only 10 get the effect; response may still be one line with aggregated accrual — see unit expansion |
limits vs win limits | rule_effects.limits = effect repetitions; once / num_* = how many times the customer can win the campaign |
| Bundle / 2x1 | Trio: 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_items | Invalid in product; use total or individual_items_pro_rata |
formula without promoPrice on line | Incomplete integration; accrual 0 on that line |
| % or ratio without application on cart | Check rule_application — usually required for line-level % |
| Same % in config, different on ticket | Check stackability (Highest Discount won another campaign) and effects[] |
Back: Reading from GET · Checklist