Tiered loyalty program
Custom events + custom data use case · For shops running a tiered loyalty program (Bronze, Silver, Gold, Platinum) who want to trigger real-time marketing scenarios on the program's events: points earned, tier change, points about to expire.
The business need
Your loyalty program awards points on every purchase (or gamification action: referral, customer review, newsletter signup) and moves customers up or down across 4 tiers: Bronze, Silver, Gold, Platinum. Each tier unlocks benefits (free shipping, permanent promo code, early access to private sales).
You want to use ShopiMind to:
- Notify the customer in real time when they earn points ("+50 points for your order, balance = 1290 pts").
- Congratulate the customer on a tier upgrade ("You're now Gold 🎉").
- Recover a customer who drops down a tier ("30 days left to regain your Gold status").
- Remind the customer about points about to expire ("500 points expire in 7 days").
The ShopiMind solution
Three building blocks combined:
- A "LoyaltyAccount" definition (custom data) that stores the current state of the loyalty account: balance, tier, tier-entry date. Useful for segmentation and dynamic display in messages.
- Several event types declared via
createEvent, one per key moment of the program. - Automation scenarios configured in the ShopiMind dashboard, subscribed to the events and personalizing their content with
LoyaltyAccountattributes.
The data model
"LoyaltyAccount" definition
The current program state for a contact. One row per enrolled customer.
contact_id(number) — relation to the Contact entity.points_balance(number) — current points balance.tier(text) —Bronze,Silver,GoldorPlatinum.tier_since(date) — date of entry into the current tier.next_expiry_date(date) — expiration date of the next batch of points.next_expiry_amount(number) — number of points that will expire on that date.- Uniqueness key:
contact_id— one loyalty account per contact.
Event types
code_name | Triggered by your system | metaData |
|---|---|---|
loyalty.points_earned | On every points credit (purchase, referral, etc.) | points_amount, reason, new_balance |
loyalty.tier_upgraded | Moving up to a higher tier | previous_tier, new_tier |
loyalty.tier_downgraded | Moving down to a lower tier | previous_tier, new_tier, grace_period_days |
loyalty.points_expiring | X days before a batch expires | points_at_risk, expiry_date |
Implementation
1. Create the "LoyaltyAccount" definition
curl -X POST 'https://core.shopimind.com/v1/custom-data-definitions' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"name": "LoyaltyAccount",
"description": "Current state of the loyalty program for a contact",
"unique_keys": ["contact_id"],
"fields": [
{ "name": "contact_id", "label": "Contact", "type": "number", "required": true },
{ "name": "points_balance", "label": "Points balance", "type": "number", "required": true },
{ "name": "tier", "label": "Tier", "type": "text", "required": true },
{ "name": "tier_since", "label": "Tier since", "type": "date", "required": false },
{ "name": "next_expiry_date", "label": "Next expiry date", "type": "date", "required": false },
{ "name": "next_expiry_amount", "label": "Points at risk", "type": "number", "required": false }
],
"relationships": [
{ "sourceField": "contact_id", "targetSchemaType": "system", "targetSchema": "contacts", "targetField": "id_contact" }
]
}'Note the returned id_definition — let's say 312.
2. Declare the event types
One call per event type. The code_name values used here follow the recommended Resource.Action notation — you can also use _ as a separator.
# 1/ Points earned
curl -X POST 'https://core.shopimind.com/v1/events' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"name": "Loyalty — points earned",
"code_name": "loyalty.points_earned",
"properties": {
"metaData": [
{ "name": "points_amount", "type": "number", "required": true,
"description": "Number of points credited by this action" },
{ "name": "reason", "type": "text", "required": true,
"description": "purchase | referral | review | newsletter_signup | birthday" },
{ "name": "new_balance", "type": "number", "required": true,
"description": "Balance after credit" }
]
}
}'
# 2/ Tier upgraded
curl -X POST 'https://core.shopimind.com/v1/events' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"name": "Loyalty — tier upgraded",
"code_name": "loyalty.tier_upgraded",
"properties": {
"metaData": [
{ "name": "previous_tier", "type": "text", "required": true },
{ "name": "new_tier", "type": "text", "required": true }
]
}
}'
# 3/ Tier downgraded
curl -X POST 'https://core.shopimind.com/v1/events' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"name": "Loyalty — tier downgraded",
"code_name": "loyalty.tier_downgraded",
"properties": {
"metaData": [
{ "name": "previous_tier", "type": "text", "required": true },
{ "name": "new_tier", "type": "text", "required": true },
{ "name": "grace_period_days", "type": "number", "required": false,
"description": "Days left to regain the former tier" }
]
}
}'
# 4/ Points expiring
curl -X POST 'https://core.shopimind.com/v1/events' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"name": "Loyalty — points expiring",
"code_name": "loyalty.points_expiring",
"properties": {
"metaData": [
{ "name": "points_at_risk", "type": "number", "required": true },
{ "name": "expiry_date", "type": "date", "required": true }
]
}
}'3. Configure the scenarios in the dashboard
In the ShopiMind dashboard, create one automation scenario per key moment. For each scenario:
- Choose the "Custom event" trigger type and select the matching
code_name(loyalty.points_earned,loyalty.tier_upgraded, etc.). - Build the message by combining the event payload (
{var=metaData.points_amount},{var=metaData.new_tier}) and the loyalty account attributes ({var=LoyaltyAccount.tier},{var=LoyaltyAccount.points_balance}).
On save, ShopiMind automatically wires the event to the scenario — no extra API call is needed on your system's side.
4. Synchronize the loyalty account state
On every state mutation (ideally just before or just after firing the event), upsert the LoyaltyAccount record. The uniqueness key contact_id guarantees the existing record is updated, not duplicated.
curl -X POST 'https://core.shopimind.com/v1/custom-data-records/312' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '[
{
"contact_id": { "by": "id_customer", "value": "CUST-A4521" },
"points_balance": 1290,
"tier": "Gold",
"tier_since": "2025-11-12",
"next_expiry_date": "2026-12-31",
"next_expiry_amount": 320
}
]'This synchronization lets scenarios display the up-to-date balance and tier in their messages, and lets you filter or segment on these attributes.
5. Fire the events
For every action on your loyalty-system side, call triggerEvent with the matching code_name.
Purchase with points earned:
curl -X POST 'https://core.shopimind.com/v1/events/trigger/loyalty.points_earned' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"contact": { "email": "marie.dupont@example.com" },
"metaData": {
"points_amount": 50,
"reason": "purchase",
"new_balance": 1290
}
}'Tier upgrade:
curl -X POST 'https://core.shopimind.com/v1/events/trigger/loyalty.tier_upgraded' \
-H 'spm-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{
"contact": { "email": "marie.dupont@example.com" },
"metaData": {
"previous_tier": "Silver",
"new_tier": "Gold"
}
}'All active scenarios subscribed to this code_name are triggered in near real-time.
Message content personalization
In the "Gold upgrade" scenario, you can combine the event payload and the loyalty account attributes:
Congratulations {var=contact.first_name}!
You've just reached {var=metaData.new_tier} status 🎉
You now benefit from:
- Free shipping on every order
- 10% permanent discount
- Early access to private sales
Your current balance: {var=LoyaltyAccount.points_balance} pointsShopiMind resolves metaData.* (current event payload) and LoyaltyAccount.* (custom account state) at runtime — no conditional logic needed in the template.
Going further
Diagnose a silent scenario
If a scenario doesn't seem to trigger after a triggerEvent:
- Check via
listEventHistoriesthat the event actually landed (filter bycode_name). - Confirm the target (
contactorvisitor) was resolved — a payload with an unknownemaillogs the event but doesn't trigger any scenario. - Make sure the scenario is in active status on the dashboard.
Summary
| Step | Endpoint | When |
|---|---|---|
Create LoyaltyAccount definition | createCustomDataDefinition | Once |
| Declare the 4 event types | createEvent | Once |
| Synchronize account state | saveCustomDataRecords | On every state mutation |
| Fire a loyalty event | triggerEvent | On every user action |
| Inspect the trigger history | listEventHistories | Diagnosis |