Campaigns

Campaigns define how members are rewarded — points, tangible rewards, or both. A campaign has an earningRule (how to award), optional targeting (segments, stores, schedule), and an optional redemptionRule (how to burn). Only ACTIVE campaigns fire during evaluate().

State machine

DRAFT → ACTIVE → PAUSED → ACTIVE
ACTIVE → ARCHIVED (terminal)

Endpoints

GET
/tenants/{tenantId}/campaigns

List all campaigns. Filter by state: DRAFT · ACTIVE · PAUSED · ARCHIVED.

POST
/tenants/{tenantId}/campaigns

Create a campaign. Starts in DRAFT state. Must be activated before it can fire.

GET
/tenants/{tenantId}/campaigns/{campaignId}

Get a campaign by ID including full earningRule and redemptionRule configuration.

PATCH
/tenants/{tenantId}/campaigns/{campaignId}

Update a campaign — name, rules, budget, schedule, or targeting. ACTIVE campaigns can be patched.

DELETE
/tenants/{tenantId}/campaigns/{campaignId}

Archive a campaign. Archived campaigns cannot be reactivated.

POST
/tenants/{tenantId}/campaigns/{campaignId}/activate

Transition a DRAFT campaign to ACTIVE. Required before evaluate() can match the campaign.

POST
/tenants/{tenantId}/campaigns/{campaignId}/pause

Pause an active campaign. evaluate() will skip paused campaigns.

POST
/tenants/{tenantId}/campaigns/{campaignId}/resume

Resume a paused campaign.

Earning Rule Types

Set earningRule.type when creating a campaign. Each type has its own required fields.

typeDescriptionKey field(s)
SPEND_PERCENTAGEPoints as a % of basket. 1% = 1 point per R1. Most common earn rule."percentage": 1
FIXED_POINTSFlat award per qualifying transaction regardless of basket value."points": 100
VISIT_FIXEDSame as FIXED_POINTS but tracked separately for visit analytics."points": 50
PRODUCT_PURCHASEPoints per qualifying SKU or category in the products[] line items."points": 5, "skus": ["SKU-001"]
RANDOMAward a random points amount within a range, with an optional win probability (0–1)."minPoints": 50, "maxPoints": 500, "probability": 0.1
STAMP_CARDN stamps → completion reward or points. Tracks progress per member. Set triggerEventKey to stamp on a custom event instead of transactions."stampsRequired": 10, "rewardOnCompletion": {...}, "triggerEventKey": "event.attended"
SURPRISE_DELIGHTEvery Nth qualifying transaction triggers a reward — deterministic cadence, not probability."rewardRules": [{ "everyNth": 5, "scope": "MEMBER", "reward": {...} }]
SIGNUP_BONUSLifecycle rule: fires once on the member's first SIGNUP event. Can award points or a tangible reward."points": 500 | "rewardType": "COUPON_VOUCHER"
ON_DEMANDLifecycle rule: fires on any member event (BIRTHDAY, ANNIVERSARY, CUSTOM, …). Can award points or a reward."points": 200 | "rewardType": "DISCOUNT_VOUCHER"
REFERRAL_BONUSLifecycle rule: fires on a REFERRAL event. Awards points to the referred member and optionally the referrer."points": 300, "referrerPoints": 100
EVENT_TRIGGERFires when a custom event matches eventKey, with optional payload conditions. Awards points or a tangible reward."eventKey": "purchase.completed", "reward": {...}

Targeting

Campaigns can target specific audiences and stores:

targetSegmentIdsstring[]Only fire for members in one of these segment UUIDs.
storeIdsstring[]Only fire when storeRef in the evaluate() request matches.
startDateISO 8601Campaign does not fire before this date.
endDateISO 8601Campaign does not fire after this date.
minSpendintegerMinimum basket amount (cents) to qualify.
maxBudgetCentsintegerAuto-pause when cumulative discounts reach this amount.

Create a campaign

cURL
curl -X POST "https://loyalty-engine-production-e5cb.up.railway.app/v1/tenants/$TENANT_ID/campaigns" \
  -H "X-API-Key: sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":        "Spend & Earn",
    "earningRule": {
      "type":       "SPEND_PERCENTAGE",
      "percentage": 1
    }
  }'

Activate it

cURL
curl -X POST "https://loyalty-engine-production-e5cb.up.railway.app/v1/tenants/$TENANT_ID/campaigns/$CAMPAIGN_ID/activate" \
  -H "X-API-Key: sk_test_YOUR_KEY"

Campaigns start in DRAFT state. Call /activate before testing with evaluate(). If your first evaluate() returns "campaigns": [], check the _debug field for why the campaign skipped.

Campaigns — API Reference