Promotion rules
Who a promotion reaches, and which lines it touches, is decided in four places. Each answers a different question, and they all have to pass.
| Layer | Question it answers | Where it lives |
|---|---|---|
| Audience | Which customers is it for? | audience_id, plus customer_includes / customer_excludes |
| Eligibility | Which of them may redeem it? | shape.eligibility |
| Scope | Which lines of their bill does it touch? | scope |
| Shape conditions | What must the bill look like for it to fire? | fields of the shape itself |
Audience: which customers
An audience is a reusable customer population, created with
POST /v1/audiences and referenced from a promotion by audience_id. Editing a
shared audience affects every promotion that uses it;
POST /v1/promotions/{id}/audience/fork gives a promotion a private copy.
An audience's definition is either match_all: true (every customer) or a
root rule group. A group combines its children with op and or or, can
be inverted with negate, and its children are further groups or leaves. A leaf
tests one fact:
Code
The dimensions are customer.tag, customer.type, customer.country,
customer.status, customer.id, customer.created_at, customer.currency,
customer.wallet_mode, customer.spend_total, customer.spend_month,
customer.subscription_count, customer.age_days, subscription.plan_id and
subscription.status. Each declares which operators it accepts, from in,
not_in, has_any, has_all, has_none, gt, gte, lt, lte,
exists_with and not_exists_with. Values are always strings.
POST /v1/audiences/preview counts who a definition would match before you save
it, and GET /v1/audiences/{id}/membership asks whether one customer is in it.
On the promotion itself, customer_includes admits named customers regardless
of the audience and customer_excludes refuses them. A customer on both lists is
refused as an authoring error rather than resolved.
An audience with no rules is refused, not read as everyone
An empty rule tree is refused. To mean "everyone", set match_all: true, which
the builder offers as Every customer. An empty rule set is treated as a
mistake, because it usually is one.
Eligibility: who may redeem
shape.eligibility adds conditions every redemption is checked against, on every
path a customer can come to hold the promotion:
first_purchase: only a customer who has never been invoiced. The welcome-offer condition; pair it with afree_periodsshape for a first-month-free promotion.plans: only a customer currently holding one of these plans. Use it to make an offer that exists for one tier only.requires_verification: only a customer currently holding a verification of this kind (for examplestudent). This is how student, non-profit and reseller pricing is expressed: the verification is the proof, and the promotion reads it.
A redemption that fails a condition is refused by name, for example
PROMOTION_NOT_ELIGIBLE.
Scope: which lines
scope narrows the lines a promotion may touch: products (product ids),
price_keys and product tags, where a line matches a tag on intersection.
An empty list means any. Omit scope for the whole catalogue. It is frozen once
the promotion is published.
negotiated_prices (exclude by default) decides whether the promotion may cut
a hand-negotiated price at all.
Shape conditions: what the bill must look like
Some conditions belong to the shape, because they only make sense for it:
Minimum spend
A line_discount with min_spend fires only when the bill's net before any
promotion reaches the amount, stated as a money table.
Code
Thresholds on units or spend
threshold and rebate count a measure (units or spend) over a window
(lifetime, billing_period, calendar_month, calendar_year,
first_n_periods) and act once it reaches threshold_units or
threshold_spend. A threshold cuts the lines beyond the crossing; a rebate gives
back a percentage of the whole window. Use them for growth rewards: cross
1,000,000 API calls and something happens.
Bundles: products bought together
A bundle names a set of legs (each a product, an optional price_key and a
quantity) that must all be present, and a target line the reward lands on:
free, percent or fixed.
Code
grace handles the awkward real case where the products are bought days apart:
grace_direction lookback reaches an already-invoiced line, lookahead holds
the redemption pending until the target arrives, both does both, each within
grace_days. With none they must land together. repeat per_set rewards one
target per completed set; once rewards it once. Matching on price_key is how
a keyed product bundle works: two domains of given extensions, and
the third is free.
Check before you publish
Before publishing anything with a budget, POST /v1/promotions/preview runs a
customer's promotions, and optionally the draft you send as promotion, against
the bill lines you state, and tells you what would happen, including what
matched and did nothing, and why. Nothing is written. On a promotion whose
eligibility you are unsure about, that is a great deal cheaper than finding out
from the invoices.
Reference
| Topic | When you need it |
|---|---|
| Promotion effects | What happens once a customer qualifies |
| Promotions | Distribution, window, limits and the lifecycle |
| Key sets | The price keys a scope or bundle matches on |
| Audiences API | Every dimension and operator |
| Promotions API | Every field, plus preview |