Composable campaigns
Composable campaigns
Contents
- Overview
- Key concepts
- Rewards handling behaviour
- Configuration flow
- Complete example
- Competition draw example
- Distribution rules
- Value calculation rules
- Endpoints reference
Overview
Composable campaigns represent a modular approach to campaign configuration in Spaaza. Unlike traditional campaign types that bundle all configuration in a single entity, composable campaigns separate targeting, conditions, and rewards into independent, reusable components.
A fully configured composable campaign consists of:
- Campaign - The base campaign entity with
type: "composable" - Assigned Groups - Collections of product barcodes for targeting
- Restrictions - Rules that determine when the campaign applies
- Reward Methods - How rewards are calculated and distributed
This modular architecture provides several benefits: assigned groups can be reused across multiple campaigns, restrictions can be combined to create complex eligibility rules, and reward methods can be configured independently of the campaign's targeting logic.
Key concepts
Campaign context
The context field determines how and when the campaign fires. It is set once at creation time via the add-campaign endpoint and cannot be changed afterwards.
| Context | Description |
|---|---|
basket | (default) Campaign fires on the addBasket event when a basket is submitted. Used for cashback, vouchers, and basket-driven rewards. |
interaction | Campaign fires only when called via the interact-campaign endpoint. Used for scratch-card / competition-draw flows. |
internal | Placeholder for future campaigns that are not driven by basket or interaction events. Currently accepts no restrictions. |
The context also restricts which restriction types can be set on the campaign and its reward methods. See Allowed restriction types per context.
Assigned groups
Assigned groups define collections of product barcodes that can be used for campaign targeting. They are chain-scoped entities that can be reused across multiple campaigns.
| Field | Type | Description |
|---|---|---|
| name | string | Group name (max 255 characters, required for new groups) |
| type | string | qualify (for use in restrictions) or redeem (for use in reward methods) |
| required_matches | integer | Minimum number of matching items required in the basket (default 0) |
| barcodes | array | Array of product barcode strings (duplicates not allowed) |
| excludes_barcode_matches | boolean | When true, the group matches items that are NOT in the barcode list (default false) |
The type field determines how the assigned group is used:
qualifygroups are used in restrictions to determine which baskets qualify for the campaignredeemgroups are used in reward methods to determine which items receive the reward
Restrictions
Restrictions define conditions under which a campaign applies. Multiple restrictions can be added to a campaign, and all restrictions must be satisfied for the campaign to activate.
Campaign-level restrictions are set inline when creating or updating a campaign via add-campaign or alter-campaign. The available campaign-level restriction types are:
| Type | Description | Configuration Fields |
|---|---|---|
| basket_item | Restrict by items in the basket | assigned_groups (array of IDs), max_basket_items_considered |
| basket_total_value | Restrict by basket total value | minimum_basket_total_value, maximum_basket_total_value |
| business | Restrict by business location | business_ids, business_formats, business_regions |
| currency | Restrict by transaction currency | currencies (array of 3-letter ISO codes) |
| segment | Restrict by user segment membership | user_segment_id, excluded_user_segment_id |
The following restriction types are configured on reward methods (via alter-reward-method), not on campaigns directly:
| Type | Description | Configuration Fields |
|---|---|---|
| budget | Set a budget cap with optional time period | budget (float), budget_period_quantity, budget_period_quantity_unit |
| reward_limit | Limit how many times a user can earn from this reward method | quantity (integer, required), unit, scale |
| usage_cost | Deduct a cost from a wallet when a reward is issued | wallet_id (integer), cost (integer, >= 1) |
| win_chance | Probability that the reward method fires when eligible | method (string, required, must be "basic"), base_chance (float, required, between 0 and 1) |
Reward limit units: year, month, week, day, hour, calendar_day.
Reward limit scale: Optional positive integer multiplier for the unit. For example, unit: "month" with scale: 3 applies the limit over a rolling 3-month period. When omitted, scale defaults to 1.
Usage cost: The usage_cost restriction deducts the configured cost amount from the user's balance in the specified wallet each time a reward is issued. If the user does not have sufficient balance, the reward method will not activate. The wallet_id must reference a valid wallet belonging to the same chain as the campaign.
Win chance: The win_chance restriction makes a reward method fire only some of the time, even when all other restrictions are satisfied. When the reward method is evaluated, a uniform random number between 0 and 1 is drawn; if it is less than or equal to base_chance, the reward method fires, otherwise it is skipped for that evaluation. base_chance: 0.05 means a 5% chance of firing, base_chance: 1 means it always fires (equivalent to no win_chance restriction), and base_chance: 0 means it never fires. The only supported method is currently "basic". For basket-context reward methods the roll is performed when the basket price is calculated, so the outcome quoted to the user carries through to the basket commit.
Budget limitation: Budget enforcement currently counts voucher amounts only. A budget restriction on a wallet_contribution reward method does not block rewards because UserPurchaseProgress contributions are not included in the budget balance. Use budget restrictions on voucher-issuing reward method types (instant_*, deferred_*, honour_voucher) for now.
Which restriction types are allowed depends on the campaign's context. See Allowed restriction types per context.
Restrictions can also be set inline when creating or updating a composable campaign via the add-campaign or alter-campaign endpoints. Pass a restrictions object keyed by restriction type:
{
"campaign_id": 100,
"restrictions": {
"currency": {
"currencies": ["EUR"]
},
"basket_item": {
"assigned_groups": [1]
}
}
}
When restrictions are set inline, they replace all existing restrictions on the campaign.
Allowed restriction types per context
The campaign's context determines which restriction types are valid. Attempting to set a restriction that is not allowed for the campaign's context returns parameter_invalid.
Campaign-level restrictions:
| Restriction Type | basket | interaction | internal |
|---|---|---|---|
basket_item | yes | no | no |
basket_total_value | yes | no | no |
currency | yes | no | no |
business | yes | no | no |
business_format | yes | no | no |
business_region | yes | no | no |
segment | yes | yes | no |
Reward-method-level restrictions:
| Restriction Type | basket | interaction | internal |
|---|---|---|---|
basket_item | yes | no | no |
currency | yes | no | no |
business | yes | no | no |
business_format | yes | no | no |
business_region | yes | no | no |
budget | yes | yes | no |
reward_limit | yes | yes | no |
usage_cost | yes | yes | no |
win_chance | yes | yes | no |
Reward methods
Reward methods define how rewards are calculated and distributed to users.
| Field | Type | Description |
|---|---|---|
| type | string | The reward method type (see Reward method types) |
| priority | integer | Processing order (lower numbers are processed first) |
| usage_limit | integer | Maximum number of times this reward method can issue rewards. null means no limit |
| value | float | The reward value. For percentage types, a decimal between 0 and 1 (e.g., 0.05 for 5%) |
| value_calculation_rule | string | items_value (default, matched items only), basket_value (entire basket), or fixed_value (flat per-matched-item reward, wallet_contribution only) |
| distribution_rule | string | all_items, cheapest_item, or most_expensive |
| selection | string | Item selection order: high_to_low or low_to_high |
| recipient_wallet_id | integer | The wallet ID to credit (required for wallet_contribution type) |
| honour_code | string | The code shown to the user or scanned by staff (for honour_voucher type) |
| voucher_claimed_by_default | boolean | Whether issued vouchers start as claimed (auto-redeemed) or generated (must be claimed). See Voucher shaping configuration |
| voucher_expiry_seconds | integer | Duration in seconds until issued vouchers expire. See Voucher shaping configuration |
| voucher_expiry_date | string | Fixed expiry date (YYYY-MM-DD) for issued vouchers. See Voucher shaping configuration |
| spend_on_promotional_items | boolean | Whether issued vouchers can redeem against promotional-price items. See Voucher shaping configuration |
| return_reclaims_earned_rewards | boolean | Whether earned wallet progress is clawed back on return. See Return clawback configuration |
| restrictions | object | Optional restrictions on the reward method (see Reward method restrictions) |
| metadata | object | Display information: title, subtitle, description, image_url, notes, log_message |
Reward method types
The type field on a reward method determines how the reward is calculated and delivered:
Instant voucher types - Create a voucher that is applied immediately to the current basket:
| Type | Description |
|---|---|
| instant_fixed_price | Applies a fixed price discount to matched items in the basket |
| instant_fixed_discount | Applies a fixed amount discount to the basket |
| instant_percentage | Applies a percentage discount to the basket |
Deferred voucher types - Create a voucher that can be redeemed in a future transaction:
| Type | Description |
|---|---|
| deferred_fixed_price | Creates a voucher with a fixed price discount for a future purchase |
| deferred_fixed_discount | Creates a voucher with a fixed amount discount for a future purchase |
| deferred_percentage | Creates a voucher with a percentage discount for a future purchase |
Other types:
| Type | Description |
|---|---|
| wallet_contribution | Credits a wallet or points wallet with the calculated reward value |
| honour_voucher | Creates a voucher with an honour code that can be validated externally (e.g., by staff) |
Voucher shaping configuration
Voucher-issuing reward methods (instant_* and deferred_* types) support configuration fields that control how the issued voucher behaves. These fields are set on the reward method via the alter-reward-method endpoint and take effect on every voucher that the reward method issues.
Traditional (non-composable) campaign types configure these behaviours at the campaign level (e.g. created_voucher_claimed_by_default, voucher_expiry_days, spend_on_promotional_items). In composable campaigns the same behaviours are configured per reward method instead, giving independent control when a campaign has multiple reward methods.
| Field | Type | Allowed on | Default | Description |
|---|---|---|---|---|
voucher_claimed_by_default | boolean | deferred_* types only | false | When true, vouchers are created with status claimed and are automatically applied to the next qualifying transaction. When false, vouchers are created with status generated and the shopper must explicitly claim them before they can be redeemed. Not configurable on instant_* types (instant vouchers are always claimed because they apply to the current basket). |
voucher_expiry_seconds | integer | deferred_* types only | — | Duration in seconds from voucher issuance until the voucher expires. Must be a positive integer. Mutually exclusive with voucher_expiry_date. |
voucher_expiry_date | string | deferred_* types only | — | Absolute expiry date in YYYY-MM-DD format. Every voucher issued by this reward method expires at the end of this date, regardless of when it was earned. Must not be in the past. Mutually exclusive with voucher_expiry_seconds. |
spend_on_promotional_items | boolean | instant_* and deferred_* types | true | When false, the issued voucher cannot redeem against the promotional-price portion of a basket — only the non-promotional total is considered when computing the maximum redeemable amount. |
When neither voucher_expiry_seconds nor voucher_expiry_date is set on the reward method, issued vouchers default to expiring 90 days after issuance.
To clear a previously set expiry field, send its value as null. Because voucher_expiry_seconds and voucher_expiry_date are mutually exclusive, you may need to clear one before setting the other.
Example: deferred voucher with claimed-by-default and 30-day expiry
{
"campaign_id": 100,
"type": "deferred_percentage",
"priority": 1,
"configuration": {
"value": 0.10,
"distribution_rule": "all_items",
"voucher_claimed_by_default": true,
"voucher_expiry_seconds": 2592000
},
"metadata": {
"title": "10% Off Next Purchase",
"description": "Automatically applied to your next visit within 30 days"
}
}
Example: deferred voucher with fixed expiry date and no promotional spend
{
"campaign_id": 100,
"type": "deferred_fixed_discount",
"priority": 1,
"configuration": {
"value": 5.00,
"distribution_rule": "all_items",
"voucher_expiry_date": "2026-12-31",
"spend_on_promotional_items": false
},
"metadata": {
"title": "5 EUR Off",
"description": "Valid until end of year, excludes sale items"
}
}
Return clawback configuration
wallet_contribution reward methods support a configuration field that controls whether earned wallet progress (points or balance) is clawed back when the earning purchase is returned.
| Field | Type | Allowed on | Default | Description |
|---|---|---|---|---|
return_reclaims_earned_rewards | boolean | wallet_contribution only | true | When true (or unset), wallet progress earned by this reward method is reclaimed if the earning purchase is later returned. When false, the earned progress is kept even after a return. |
Traditional campaign types configure this at the campaign level via return_reclaims_earned_reward. In composable campaigns the reward-method-level setting takes precedence. If no reward method is recorded on the purchase progress (e.g. for progress earned before this feature), the campaign-level flag is used as a fallback.
Example: wallet contribution that keeps earned points on return
{
"campaign_id": 100,
"type": "wallet_contribution",
"priority": 1,
"configuration": {
"value": 0.05,
"value_calculation_rule": "items_value",
"distribution_rule": "all_items",
"recipient_wallet_id": 42,
"return_reclaims_earned_rewards": false
},
"metadata": {
"title": "5% Loyalty Points",
"description": "Points are yours to keep, even if you return your purchase"
}
}
Reward method restrictions
Reward methods can have their own restrictions. These include spend-context restrictions (evaluated when a deferred voucher is redeemed) as well as budget, reward limit, and usage cost restrictions that control how often and under what conditions the reward method can issue rewards.
The supported restriction types depend on the parent campaign's context (see Allowed restriction types per context). For basket context campaigns, the following are available:
| Type | Description | Configuration Fields |
|---|---|---|
| basket_item | Restrict by items in basket | assigned_groups (array of IDs with type redeem) |
| business | Restrict by business location | business_ids, business_formats, business_regions |
| business_format | Restrict by store format | business_formats |
| business_region | Restrict by store region | business_regions |
| currency | Restrict by currency | currencies (array of 3-letter ISO codes) |
| budget | Set a budget cap with optional time period | budget (float), budget_period_quantity, budget_period_quantity_unit |
| reward_limit | Limit how many times a user can earn from this reward method | quantity (integer, required), unit, scale |
| usage_cost | Deduct a cost from a wallet when a reward is issued | wallet_id (integer), cost (integer, >= 1) |
| win_chance | Probability that the reward method fires when eligible | method (string, required, must be "basic"), base_chance (float, required, between 0 and 1) |
For interaction context campaigns, only budget, reward_limit, usage_cost, and win_chance are available on reward methods.
Reward method restrictions are set via the restrictions field when creating or updating a reward method:
{
"campaign_id": 100,
"type": "deferred_percentage",
"priority": 1,
"restrictions": {
"currency": {
"currencies": ["EUR"]
},
"business": {
"business_ids": [10, 20]
},
"reward_limit": {
"quantity": 5,
"unit": "month"
},
"usage_cost": {
"wallet_id": 42,
"cost": 100
},
"win_chance": {
"method": "basic",
"base_chance": 0.05
}
},
"configuration": {
"value": 0.10,
"value_calculation_rule": "items_value",
"distribution_rule": "all_items"
}
}
Rewards handling behaviour
The rewards_handling_behaviour field on a composable campaign controls how its reward methods are evaluated. This field is set when creating or updating the campaign.
independent (default)
Each reward method is evaluated independently. All eligible reward methods are processed in priority order, and each one that matches issues its own reward. This is the standard behaviour for campaigns that should issue multiple rewards per transaction (e.g., cashback on different product groups).
random_draw
A single reward method is randomly selected from the eligible candidates. This enables competition-style campaigns where each interaction results in one randomly chosen prize. The flow is:
- Eligible reward methods are determined based on remaining capacity (
usage_limit) - One reward method is randomly selected from the eligible candidates
- The selected reward method issues its reward directly to the user
- If a
usage_costrestriction is configured, the cost is deducted from the user's wallet
Composable campaigns with context: "interaction" can be triggered via the interact-campaign endpoint without requiring a basket. When paired with random_draw, this is useful for scratch-card or prize-draw style interactions where the user does not need to make a purchase. Interaction campaigns can also use independent behaviour when every eligible reward method should be processed for the interaction.
Configuration flow
The typical setup order for a composable campaign is:
- Create a wallet (if one does not already exist) - This will be the destination for earned rewards
- Create the base campaign with
type: "composable" - Create assigned groups with the product barcodes that should qualify for or receive rewards
- Add restrictions referencing the assigned groups to define eligibility rules
- Configure the reward method to define how rewards are calculated and distributed
For competition draw campaigns, the setup order is:
- Create a wallet (if one does not already exist) - For points-based entry costs and/or wallet rewards
- Create the base campaign with
type: "composable",context: "interaction", andrewards_handling_behaviour: "random_draw" - Configure reward methods with
usage_limitset to define the prize pool, and optionally add ausage_costrestriction on each reward method to require points per draw