Promotions
A promotion is a versioned ruleset that decides whether to discount, credit, or modify an invoice. One promotion can carry multiple typed effects (apply a percentage discount AND post a wallet credit AND give one free period); whether and how it fires is decided by typed conditions evaluated against the customer / subscription / invoice context.
Lifecycle
Code
| Status | Meaning |
|---|---|
draft | Editable, not matchable |
active | The condition engine evaluates this promotion on every relevant event |
expired | valid_to passed - existing redemptions keep running their term, no new redemptions |
archived | Permanently retired |
Activate / deactivate transitions go through POST /v1/promotions/{id}/activate and /deactivate rather than a generic PUT, so transition-side effects (audit, scheduling) run consistently. Lifecycle events: promotion.created / updated / activated / archived / expired / phase_changed.
Conditions
Conditions are typed sub-objects under conditions - not a free-form rule list. condition_mode decides how multiple non-nil conditions combine:
condition_mode | Meaning |
|---|---|
ALL (default) | Every non-nil condition must pass |
ANY | At least one non-nil condition must pass |
The eight condition types:
| Field | Matches when |
|---|---|
minimum_spend | Spend ≥ amount, scoped per per_invoice (default), period, or lifetime |
subscription_plan | Subscription is on a specific plan or one of a set |
external_verification | The customer has a passing verification of a given type (KYC, business, tax-ID, …) |
product_combination | The subscription includes a particular product set |
quantity_threshold | Aggregated quantity on a metered product is at/above a value |
customer_attribute | A field on the customer record (incl. custom fields) matches a value |
first_purchase | The customer's first invoice |
usage_threshold | A metered usage value crossed a threshold during the current period |
Date ranges are not a condition type - they live on the promotion itself as
valid_from/valid_to. Coupon codes are also not a condition - they're set on the promotion'scodefield withvisibility: "COUPON"(see Visibility).
Effects
Effects are also typed and can compose. A single promotion may set any combination of the seven effect fields and they all apply, in this canonical order:
Code
| Effect | What it does |
|---|---|
percentage_discount | N% off matching invoice lines |
fixed_discount | Fixed amount off matching invoice lines |
price_override | Replace the line price with a flat amount (used for "first month at $5") |
free_product | Attach an extra product to the subscription at zero price |
free_periods | Skip charges for N billing periods |
wallet_credit | Credit the customer's wallet on activation |
usage_credits | Grant N units of allowance on a usage product |
The canonical order matters when multiple effects discount the same line (e.g. percentage applies first, then fixed-amount on top of the discounted base).
Visibility, codes & tokens
visibility decides how a promotion gets activated:
| Visibility | Behaviour |
|---|---|
PUBLIC | Auto-evaluated against every relevant event; no customer action needed |
INTERNAL | Only fires when applied by an operator (admin / support tooling) |
COUPON | Requires the customer to supply a code at the right moment; the promotion's code field is the value to match |
For coupon-style promotions, there are two redemption paths:
- Code-based - the customer supplies
codetoPOST /v1/promotions/{id}/redeem(or implicitly via the checkout flow). - Token-based - generate one or more single-use redemption tokens with
POST /v1/promotions/{id}/tokens, then redeem withPOST /v1/promotions/redeem-token. Tokens are useful for one-off invitations or when you need an opaque, link-friendly redemption credential.
Application timing
Code
application_timing controls when the redeemed effect lands on an invoice. For phased promotions, each phase carries the same field independently via phase_transition_timing (see below).
Budget & redemption caps
| Field | Behaviour |
|---|---|
max_redemptions | Hard cap on total redemptions across all customers |
max_redemptions_per_customer | Hard per-customer cap |
max_budget | Hard cap on total monetary impact (currency follows the org's reporting currency) |
current_redemptions / current_budget_used | Read-only counters maintained by the engine |
budget_behavior | SKIP - once max_budget is hit, new redemptions are rejected. PARTIAL - the next redemption gets a reduced effect that lands exactly on the budget line, then subsequent ones skip. |
When max_budget is exhausted, the engine emits promotion.budget_exhausted. Per-customer redemption transitions emit promotion.redeemed, promotion.redemption_revoked, promotion.redemption_reactivated, promotion.redemption_dormant, promotion.redemption_cap_reached, promotion.redemption_free_periods_exhausted, plus promotion.effect_applied whenever an effect lands on an invoice line.
Stacking & priority
| Field | Behaviour |
|---|---|
stackable | When true, the promotion can combine with other matching promotions on the same invoice. When false, it competes - the highest-priority single match wins and others on this invoice are skipped. |
priority | Integer; higher values evaluate first. Within stackable: true, priority decides apply order across promotions; within stackable: false, priority decides which one wins the slot. |
Phased promotions
Set is_phased: true and supply phases[] to model multi-step promotions where the customer moves through a state machine - the classic loyalty-ladder ("Bronze → Silver → Gold" with bigger discounts at higher tiers) or growth-ladder ("$5 month 1 → $10 month 2 → $15 month 3").
Each PromotionPhase carries:
| Field | Notes |
|---|---|
phase_order | Position in the ladder. Phases evaluate in order. |
eligibility | Conditions that must pass for the customer to be in this phase at all. |
advance | Conditions that, when met, move the redemption to the next phase. |
regress | Conditions (with optional hysteresis bounds) that move it backwards. |
effects | The effects that apply while in this phase. |
min_dwell_days | Minimum time before advance / regress is allowed - prevents flapping. |
allow_regression | If false, the ladder is one-way (a customer who reaches a tier can't fall out). |
is_terminal | Marks the end of the ladder; no further advance possible. |
phase_transition_timing | immediate, next_cycle (default), or prorate - controls when the new phase's effects start showing on the invoice. |
Every transition is recorded as a PromotionPhaseTransition (with direction ∈ initial / advance / regress / manual, trigger_type ∈ usage_threshold / quantity_change / invoice_eval / scheduled / admin, and an optional reason). Audit-complete out of the box.
Archetypes
archetype is a typed hint for tooling - it tells the dashboard / SDK how to render the promotion and what defaults to show. It does not constrain the conditions / effects you can set; it's a label, not a class.
Available archetypes: generic, coupon_discount, bundle_discount, volume_rebate, first_purchase_credit, free_trial, cross_sell_free, loyalty_ladder, growth_ladder. The first eight are non-phased; loyalty_ladder and growth_ladder are the two phased archetypes.
Preview, validate, simulate
Three dry-run endpoints help you check a promotion before customers see it:
| Endpoint | What it returns |
|---|---|
POST /v1/promotions/{id}/preview | Resolve a promotion against a fake invoice context - see exactly which effects would apply and at what amount. |
POST /v1/promotions/validate | Static check - would this promotion definition be accepted? Used by the dashboard before clicking save. |
POST /v1/promotions/simulate | Run the engine against a real recent invoice and report the would-be outcome, without persisting a redemption. Useful for "would this campaign have produced a positive ROI?" backtests. |
Endpoints
All Promotions endpoints - covers CRUD, redemption (token + direct), preview / validate / simulate, schedule + version timeline, activate / deactivate.
Related
- Subscriptions - phased promotions ride a subscription's state machine
- Wallets - destination for
wallet_crediteffects - Verifications - gating condition for
external_verification - Invoices - where
effect_appliedevents surface as line adjustments - Customers -
customer_attributeresolves against this record