Skip to main content

Documentation Index

Fetch the complete documentation index at: https://appstleinc-aeca3e0a.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Appstle Loyalty webhooks deliver real-time HTTP POST notifications to your endpoint whenever a loyalty event occurs. The webhook infrastructure is powered by Svix, which provides automatic retries, cryptographic signature verification, and detailed delivery logs.
Webhooks are available on paid plans. Contact support@appstle.com to enable webhook access on your account.

Setting up an endpoint

1

Open webhook settings

In your Appstle Loyalty admin, go to Settings → Webhooks.
2

Add an endpoint

Click Add Endpoint and enter your publicly accessible HTTPS URL. Your endpoint must be reachable from the internet — localhost URLs will not work.
3

Choose events

Select the event types you want to subscribe to, or subscribe to all events.
4

Save

Click Save. Your endpoint will start receiving events immediately.
Your endpoint must return a 2xx status code within the timeout window. Return 200 OK immediately and process the event asynchronously to avoid timeouts under load.

How delivery works

Each webhook is an HTTP POST request with a JSON body. Svix handles delivery with:
  • Automatic retries with exponential backoff (5 attempts over 3 days on failure)
  • Unique message IDs for idempotency
  • Signed request headers for verification
  • Delivery logs and manual replay from Settings → Webhooks → Message Logs

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 events follow the same envelope:
{
  "type": "loyalty.earned",
  "data": {
    // event-specific payload
  }
}

Common payload fields

Every event’s data object includes these fields:
FieldTypeDescription
customerIdNumberShopify customer ID
customerEmailStringCustomer’s email address
noteStringOptional description of the event
pointsNumberPoints involved (earned or redeemed amount)
earnRuleIdNumberEarn rule ID that triggered points (loyalty.earned only)
redeemRuleIdNumberRedemption rule ID used (loyalty.redeemed only)
webhookEventTypeStringInternal event type name
customerLoyaltyDetailsObjectFull loyalty profile snapshot at the time of the event

customerLoyaltyDetails fields

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 none)
vipTierExpiredAtDateTimeWhen the VIP tier expires (if applicable)
referralLinkStringCustomer’s unique referral URL
referredCompletedNumberNumber of completed referrals
dobDateDate of birth in ISO 8601 format (if set)
rewardsArrayActive and past reward objects
rewardedForFacebookBooleanFacebook follow reward earned
rewardedForTwitterBooleanX/Twitter follow reward earned
rewardedForInstagramBooleanInstagram follow reward earned
rewardedForYoutubeBooleanYouTube subscribe reward earned
rewardedForTiktokBooleanTikTok follow reward earned
rewardedForNewsLetterBooleanNewsletter signup reward earned
rewardedForSmsBooleanSMS signup reward earned
rewardedForCreatingAccountBooleanAccount creation reward earned
rewardedForSharingOnFacebookBooleanFacebook share reward earned
rewardedForSharingOnXBooleanX/Twitter share reward earned
Webhook payloads include additional fields (such as storeCreditBalance, vipTierExpiredAt, rewardedForSharingOnFacebook, and rewardedForSharingOnX) that are not available in the Shopify Flow GraphQL schema. Flow receives a subset of these fields.
Each item in the rewards array contains:
FieldTypeDescription
descriptionStringReward description
discountCodeStringGenerated Shopify discount code
statusStringUNUSED, USED, or REFUNDED
pointTransactionIdNumberPoint transaction ID
pointRedeemRuleIdNumberRedemption rule ID used
orderIdStringShopify order GID where reward was used
orderNameStringOrder name (e.g., #1002)
createAtDateTimeWhen the reward was created
usedAtDateTimeWhen the reward was used (if applicable)
expireDateDateTimeWhen the reward expires
variantIdStringShopify variant GID (for free product rewards)

Example payloads

{
  "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": [],
      "rewardedForCreatingAccount": true
    }
  }
}
{
  "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": []
    }
  }
}
{
  "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"
        }
      ]
    }
  }
}
{
  "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 three headers on every request:
HeaderDescription
svix-idUnique message ID — use as an idempotency key
svix-timestampUnix timestamp of delivery
svix-signatureHMAC-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');
});
Pass the raw request body to the verification function before JSON parsing. Parsing the body first alters the byte representation and will cause verification to fail.
For Go, Ruby, PHP, Java, and C# examples, see the Svix documentation.

Idempotency

Webhooks may be delivered more than once due to network conditions or retries. Use the svix-id header as an idempotency key to safely deduplicate events in your handler.

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 attempts and replay individual events from Settings → Webhooks → Message Logs.

Local development

Use a tunneling tool such as ngrok to expose your local server during 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 are using the correct secret from the dashboard.
Not receiving eventsConfirm webhooks are enabled under Settings and your account plan includes webhook access.
Endpoint timing outReturn 200 OK immediately and process events in a background job or queue.
Duplicate eventsUse the svix-id header as an idempotency key and skip already-processed IDs.