Receive real-time HTTP notifications when loyalty events occur. Appstle Loyalty webhooks are powered by Svix — enterprise-grade webhook infrastructure with automatic retries, signature verification, and delivery monitoring.
- In your Appstle Loyalty admin, go to Settings → Webhooks
- Click Add Endpoint and enter your HTTPS endpoint URL
- Select which events to subscribe to (or subscribe to all)
- Save — your endpoint will start receiving events immediately
ℹ️ Requires Webhook Access: Webhooks are available on paid plans. Contact support@appstle.com to enable.
Webhooks are HTTP POST requests sent to your endpoint whenever a loyalty event occurs. Your endpoint must:
- Be publicly accessible via HTTPS
- Return a
2xxstatus code within the timeout window - Process events asynchronously (queue for background processing)
Powered by Svix:
- ✅ Automatic retries with exponential backoff
- ✅ Cryptographic signature verification
- ✅ Detailed delivery logs and replay
- ✅ Developer dashboard for monitoring
| Event Type | Description |
|---|---|
loyalty.sign-up | Customer joined the loyalty program |
loyalty.earned | Customer earned points for any activity |
loyalty.redeemed | Customer redeemed points for a reward |
loyalty.credits-earned | Customer earned store credits |
loyalty.vip-tier-achieved | Customer reached a new VIP tier |
loyalty.birthday-trigger | Customer's birthday reward was issued |
loyalty.referral-reward | Referring customer received a referral reward |
loyalty.referred-reward | Newly referred customer received their welcome reward |
All webhooks follow this structure:
{
"type": "loyalty.earned",
"data": {
// Event-specific payload (see examples below)
}
}All loyalty webhook payloads include these fields:
| Field | Type | Description |
|---|---|---|
customerId | Number | Shopify customer ID |
customerEmail | String | Customer's email address |
note | String | Optional note about the event |
points | Number | Points involved in this event (earned/redeemed amount) |
earnRuleId | Number | ID of the earn rule that triggered points (on loyalty.earned) |
redeemRuleId | Number | ID of the redeem rule used (on loyalty.redeemed) |
webhookEventType | String | Internal event type name |
customerLoyaltyDetails | Object | Full loyalty profile snapshot at time of event |
:::info Webhook vs Flow Schema The webhook payload includes additional fields not exposed in the Shopify Flow GraphQL schema, such as storeCreditBalance, vipTierExpiredAt, rewardedForSharingOnFacebook, and rewardedForSharingOnX. Flow receives a subset of fields defined in the extension's GraphQL schema. :::
| Field | Type | Description |
|---|---|---|
availablePoints | Number | Current redeemable points balance |
pendingPoints | Number | Points awaiting approval |
creditedPoints | Number | Total lifetime points earned |
spentAmount | Number | Total amount spent by this customer |
storeCreditBalance | Number | Current store credit balance |
currentVipTier | String | Customer's current VIP tier name (empty if no tier) |
vipTierExpiredAt | DateTime | When the current VIP tier expires (if applicable) |
referralLink | String | Customer's unique referral URL |
referredCompleted | Number | Number of completed referrals |
dob | Date | Customer's date of birth (ISO 8601, if set) |
rewards | Array | Active and past reward objects |
rewardedForFacebook | Boolean | Whether Facebook follow reward was earned |
rewardedForTwitter | Boolean | Whether X/Twitter follow reward was earned |
rewardedForInstagram | Boolean | Whether Instagram follow reward was earned |
rewardedForYoutube | Boolean | Whether YouTube subscribe reward was earned |
rewardedForTiktok | Boolean | Whether TikTok follow reward was earned |
rewardedForNewsLetter | Boolean | Whether newsletter signup reward was earned |
rewardedForSms | Boolean | Whether SMS signup reward was earned |
rewardedForCreatingAccount | Boolean | Whether account creation reward was earned |
rewardedForSharingOnFacebook | Boolean | Whether Facebook share reward was earned |
rewardedForSharingOnX | Boolean | Whether X/Twitter share reward was earned |
| Field | Type | Description |
|---|---|---|
description | String | Reward description |
discountCode | String | Generated discount code |
status | String | UNUSED, USED, or REFUNDED |
pointTransactionId | Number | ID of the point transaction |
pointRedeemRuleId | Number | ID of the redemption rule used |
orderId | String | Shopify order GID where reward was used |
orderName | String | Order name (e.g., "#1002") |
createAt | DateTime | When the reward was created |
usedAt | DateTime | When the reward was used (if redeemed) |
expireDate | DateTime | When the reward expires |
variantId | String | Shopify variant GID (for free product rewards) |
loyalty.sign-up — Customer joined the program
{
"type": "loyalty.sign-up",
"data": {
"customerId": 12345,
"customerEmail": "member@example.com",
"note": "Welcome bonus applied",
"points": 100,
"earnRuleId": null,
"redeemRuleId": null,
"customerLoyaltyDetails": {
"availablePoints": 100,
"pendingPoints": 0,
"creditedPoints": 100,
"spentAmount": 0,
"storeCreditBalance": 0,
"currentVipTier": "",
"referralLink": "https://your-store.myshopify.com?ref=abc123",
"referredCompleted": 0,
"rewards": [],
"rewardedForFacebook": false,
"rewardedForNewsLetter": false,
"rewardedForCreatingAccount": true
}
}
}loyalty.earned — Customer earned points
{
"type": "loyalty.earned",
"data": {
"customerId": 12345,
"customerEmail": "member@example.com",
"note": "Purchase reward",
"points": 250,
"earnRuleId": 7,
"redeemRuleId": null,
"customerLoyaltyDetails": {
"availablePoints": 850,
"pendingPoints": 0,
"creditedPoints": 1100,
"spentAmount": 320.00,
"currentVipTier": "Silver",
"referralLink": "https://your-store.myshopify.com?ref=abc123",
"referredCompleted": 2,
"rewards": []
}
}
}loyalty.redeemed — Customer redeemed points for a reward
{
"type": "loyalty.redeemed",
"data": {
"customerId": 12345,
"customerEmail": "member@example.com",
"note": null,
"points": 500,
"earnRuleId": null,
"redeemRuleId": 3,
"customerLoyaltyDetails": {
"availablePoints": 350,
"creditedPoints": 1100,
"currentVipTier": "Silver",
"rewards": [
{
"description": "$5 off your next order",
"discountCode": "REWARD-XXXXX",
"status": "UNUSED",
"pointRedeemRuleId": 3,
"createAt": "2026-02-15T10:30:00Z",
"expireDate": "2026-05-15T00:00:00Z"
}
]
}
}
}loyalty.vip-tier-achieved — Customer reached a new VIP tier
{
"type": "loyalty.vip-tier-achieved",
"data": {
"customerId": 12345,
"customerEmail": "member@example.com",
"note": "Reached Gold tier",
"points": 0,
"customerLoyaltyDetails": {
"availablePoints": 2100,
"creditedPoints": 5000,
"spentAmount": 1250.00,
"currentVipTier": "Gold",
"vipTierExpiredAt": "2027-01-01T00:00:00Z"
}
}
}Every webhook request is signed by Svix. Always verify the signature before processing.
Svix includes these headers on every request:
svix-id— Unique message ID (use for idempotency)svix-timestamp— Unix timestamp of deliverysvix-signature— HMAC-SHA256 signature
Find your webhook signing secret in your Appstle dashboard under Settings → Webhooks → [your endpoint].
const { Webhook } = require('svix');
const secret = 'whsec_your_signing_secret';
app.post('/webhooks/appstle-loyalty', express.raw({ type: 'application/json' }), (req, res) => {
const wh = new Webhook(secret);
let event;
try {
event = wh.verify(req.body, {
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
});
} catch (err) {
return res.status(400).send('Signature verification failed');
}
const { customerId, points, customerLoyaltyDetails } = event.data;
switch (event.type) {
case 'loyalty.vip-tier-achieved':
// Send VIP welcome email, add Shopify customer tag
break;
case 'loyalty.referral-reward':
// Notify referrer of their reward
break;
case 'loyalty.earned':
// Sync points balance to CRM
break;
}
res.status(200).send('OK');
});from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_your_signing_secret"
@app.route('/webhooks/appstle-loyalty', methods=['POST'])
def webhook():
try:
wh = Webhook(secret)
event = wh.verify(request.data, {
"svix-id": request.headers.get("svix-id"),
"svix-timestamp": request.headers.get("svix-timestamp"),
"svix-signature": request.headers.get("svix-signature"),
})
except WebhookVerificationError:
return "Verification failed", 400
if event["type"] == "loyalty.vip-tier-achieved":
tier = event["data"]["customerLoyaltyDetails"]["currentVipTier"]
# handle tier upgrade
return "OK", 200See Svix docs for Go, Ruby, PHP, Java, and C# examples.
If your endpoint returns a non-2xx response or times out, Svix retries with exponential backoff across 5 attempts over 3 days. View delivery logs and replay events from Settings → Webhooks → Message Logs in your Appstle dashboard.
Webhooks may be delivered more than once. Use the svix-id header as an idempotency key to safely deduplicate events.
ngrok http 3000
# Add https://your-id.ngrok.io/webhooks/appstle-loyalty as your endpoint in the dashboard| Issue | Solution |
|---|---|
| Signature verification fails | Use the raw request body before JSON parsing. Confirm you're using the correct secret. |
| Not receiving events | Confirm webhooks are enabled under Settings and your plan has webhook access. |
| Endpoint timing out | Return 200 OK immediately; process events asynchronously. |
| Duplicate events | Deduplicate using svix-id header. |
Need help? Contact support@appstle.com