Skip to content
Last updated

Webhooks

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.

Getting Started

  1. In your Appstle Loyalty admin, go to Settings → Webhooks
  2. Click Add Endpoint and enter your HTTPS endpoint URL
  3. Select which events to subscribe to (or subscribe to all)
  4. Save — your endpoint will start receiving events immediately

ℹ️ Requires Webhook Access: Webhooks are available on paid plans. Contact support@appstle.com to enable.

How It Works

Webhooks are HTTP POST requests sent to your endpoint whenever a loyalty event occurs. Your endpoint must:

  • Be publicly accessible via HTTPS
  • Return a 2xx status 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 Types

Event TypeDescription
loyalty.sign-upCustomer joined the loyalty program
loyalty.earnedCustomer earned points for any activity
loyalty.redeemedCustomer redeemed points for a reward
loyalty.credits-earnedCustomer earned store credits
loyalty.vip-tier-achievedCustomer reached a new VIP tier
loyalty.birthday-triggerCustomer's birthday reward was issued
loyalty.referral-rewardReferring customer received a referral reward
loyalty.referred-rewardNewly referred customer received their welcome reward

Payload Structure

All webhooks follow this structure:

{
  "type": "loyalty.earned",
  "data": {
    // Event-specific payload (see examples below)
  }
}

Payload Fields

All loyalty webhook payloads include these fields:

FieldTypeDescription
customerIdNumberShopify customer ID
customerEmailStringCustomer's email address
noteStringOptional note about the event
pointsNumberPoints involved in this event (earned/redeemed amount)
earnRuleIdNumberID of the earn rule that triggered points (on loyalty.earned)
redeemRuleIdNumberID of the redeem rule used (on loyalty.redeemed)
webhookEventTypeStringInternal event type name
customerLoyaltyDetailsObjectFull loyalty profile snapshot at time of event

customerLoyaltyDetails Object

:::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. :::

FieldTypeDescription
availablePointsNumberCurrent redeemable points balance
pendingPointsNumberPoints awaiting approval
creditedPointsNumberTotal lifetime points earned
spentAmountNumberTotal amount spent by this customer
storeCreditBalanceNumberCurrent store credit balance
currentVipTierStringCustomer's current VIP tier name (empty if no tier)
vipTierExpiredAtDateTimeWhen the current VIP tier expires (if applicable)
referralLinkStringCustomer's unique referral URL
referredCompletedNumberNumber of completed referrals
dobDateCustomer's date of birth (ISO 8601, if set)
rewardsArrayActive and past reward objects
rewardedForFacebookBooleanWhether Facebook follow reward was earned
rewardedForTwitterBooleanWhether X/Twitter follow reward was earned
rewardedForInstagramBooleanWhether Instagram follow reward was earned
rewardedForYoutubeBooleanWhether YouTube subscribe reward was earned
rewardedForTiktokBooleanWhether TikTok follow reward was earned
rewardedForNewsLetterBooleanWhether newsletter signup reward was earned
rewardedForSmsBooleanWhether SMS signup reward was earned
rewardedForCreatingAccountBooleanWhether account creation reward was earned
rewardedForSharingOnFacebookBooleanWhether Facebook share reward was earned
rewardedForSharingOnXBooleanWhether X/Twitter share reward was earned

rewards Array Items

FieldTypeDescription
descriptionStringReward description
discountCodeStringGenerated discount code
statusStringUNUSED, USED, or REFUNDED
pointTransactionIdNumberID of the point transaction
pointRedeemRuleIdNumberID of the redemption rule used
orderIdStringShopify order GID where reward was used
orderNameStringOrder name (e.g., "#1002")
createAtDateTimeWhen the reward was created
usedAtDateTimeWhen the reward was used (if redeemed)
expireDateDateTimeWhen the reward expires
variantIdStringShopify variant GID (for free product rewards)

Example Payloads

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"
    }
  }
}

Signature Verification

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 delivery
  • svix-signature — HMAC-SHA256 signature

Find your webhook signing secret in your Appstle dashboard under Settings → Webhooks → [your endpoint].

Node.js

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');
});

Python

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", 200

See Svix docs for Go, Ruby, PHP, Java, and C# examples.

Retry Schedule

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.

Idempotency

Webhooks may be delivered more than once. Use the svix-id header as an idempotency key to safely deduplicate events.

Local Development

ngrok http 3000
# Add https://your-id.ngrok.io/webhooks/appstle-loyalty as your endpoint in the dashboard

Troubleshooting

IssueSolution
Signature verification failsUse the raw request body before JSON parsing. Confirm you're using the correct secret.
Not receiving eventsConfirm webhooks are enabled under Settings and your plan has webhook access.
Endpoint timing outReturn 200 OK immediately; process events asynchronously.
Duplicate eventsDeduplicate using svix-id header.

Need help? Contact support@appstle.com