This guide covers Appstle’s Storefront API (
/apps/loyalty/cp/api/...), which runs in a signed-in customer’s browser on a page of the store’s Shopify theme. It isn’t Shopify’s Storefront GraphQL API.
A headless storefront on its own domain can’t use it, because its requests don’t pass through Shopify’s App Proxy. For a headless storefront or a server-side integration, call the Admin API (/api/external/..., authenticated with X-API-Key) from your server, as the Integration guide shows.When to build your own vs. use the widget
Appstle’s Loyalty widget (appstle-loyalty.js) is added to the storefront by the app embed and handles the points panel, rewards, referrals and discount codes on its own. Build your own frontend when you need:
- A loyalty page that matches your own account design
- Points and rewards shown in places the widget doesn’t cover, such as a header, a cart drawer or a product page
- Your own redeem flow, driven by your app’s state
How the Storefront API is reached
Storefront API calls go through Shopify’s App Proxy. Your code calls the store’s own domain, and Shopify forwards the request to Appstle with the signed-in customer’s identity attached./apps/loyalty is the default path. A merchant can change it with the Loyalty page address setting in the Loyalty app, which must match the app proxy URL in their Shopify settings. The current path is in the proxy_path_prefix shop metafield and, on any page that loads the Appstle app embed, in window._ALConfig.proxy_path_prefix (without the leading slash). The helper below reads it from there.
Authentication
The Storefront API uses no API key and no token. Appstle identifies the customer from the Shopify session:- The customer must be signed in to the store’s customer account. A call from a visitor who isn’t signed in gets
401with the body{"status":401,"detail":"Invalid authentication"}, on reads and writes alike. - Calls that change data (
POST) must send the headerX-Requested-With: XMLHttpRequest. Without it the call gets the same401, even for a signed-in customer. Reads (GET) don’t need it. - You don’t send a customer ID or email. The calls in this guide send none, and each applies to the signed-in customer.
On a store protected by a storefront password, a visitor who hasn’t entered the password gets Shopify’s password page (
200, text/html) instead of JSON. The helper below returns ok: false for it. If you write your own fetch calls, check the Content-Type before you parse a response.Rate limits
Storefront API calls are limited for each signed-in customer and for the store as a whole. The limits count every Storefront API call on the store, including the calls the Appstle widget makes. Treat these numbers as the most you should send:
Paths that differ only in a numeric ID count as one endpoint, so every
return-reward/{rewardId} call counts toward the same limit.
A call over a limit gets 429 with the body {"error":"Rate limit exceeded. Try again later."} and no Retry-After header. Wait a minute before you retry. Because the per-endpoint limits are shared by every customer on the store, don’t call the API on every page load. To show a points balance in the header, read it in Liquid from the customer_loyalty customer metafield (see Metafields and tags), and call the API when the customer opens your loyalty UI or acts in it.
A small helper
Every example below uses this helper. It builds the URL from the store’s proxy path, sendsX-Requested-With on every call, and returns the status with the parsed body so you can show an error instead of throwing. It returns ok: false when Shopify answers with an HTML page, such as the storefront password page.
Identify the customer
GET /apps/loyalty/cp/api/logged-in-customer returns the signed-in customer’s numeric Shopify customer ID as the whole body. Use it to check that someone is signed in before you show loyalty UI.
Show points and rewards
GET /apps/loyalty/cp/api/customer-loyalty is the call for a loyalty dashboard. It returns the customer’s balances, VIP tier, referral link and the rewards they hold, in one response.
By default
rewards lists only the customer’s unused rewards. To list others, add status with a comma-separated list of UNUSED, USED, EXPIRED, REFUNDED, CANCELED and SUBSCRIPTION_ACTIVE. A reward the customer returned has the status REFUNDED. Any other value returns 400 with errorKey invalid_enum:
Show the ways to earn and redeem
The earn and redeem rules a store has set up aren’t returned by a Storefront API call. Appstle publishes them to the storefront as shop metafields,appstle_loyalty.point_earn_rules and appstle_loyalty.point_redeem_rules, so you read them in Liquid, as the Metafields and tags page shows:
id is the value you send as redeemRuleId to redeem it, and customerFacingLabel is ready-made text for your UI. type says what the customer gets, such as DISCOUNT_CODE, FREE_SHIPPING, FREE_PRODUCT, DYNAMIC_DISCOUNT or STORE_CREDIT. An earn rule has name, type (for example PURCHASE, BIRTHDAY or SUBSCRIBE_NEWSLETTER), basePoints and its own customerFacingLabel.
The metafield lists every active rule, and a merchant can limit a rule to some customers, for example by customer tag or VIP tier. A customer who can’t use a rule gets a 400 when they try, so show the error to them (see Redeem points).
Redeem points
POST /apps/loyalty/cp/api/redeem-points spends the customer’s points on a reward. Send the rule’s ID as redeemRuleId.
200 with the same body as customer-loyalty, updated: on the demo store availablePoints went from 6036.93 to 5536.93 for a 500-point rule. rewards is newest first, so the new reward and its discountCode are rewards[0].
For a DYNAMIC_DISCOUNT rule, the customer chooses how many points to spend: send them as points, for example { "redeemRuleId": 1234, "points": 300 }. The discount grows with the points: the customer gets the rule’s discount for every redeemPoints points. On the demo store, a rule with redeemPoints 100, discountType FIXED and discount 1 turned "points": 100 into a reward described as $1.00 off, and availablePoints dropped by 100.
The merchant can limit how many points a customer spends at once. requiredMinimumPointsToRedeem applies when enableMinimumPointsToRedeem is true, and requiredMaximumPointsToRedeem applies when enableMaximumPointsToRedeem is true. A points value outside those limits returns 400 with a title that starts with Minimum or Maximum. Keep your points picker inside the limits, and show them with the rule’s minimumPointsLabel and maximumPointsLabel, replacing {{points}} with the limit.
Here is a FREE_PRODUCT rule from the point_redeem_rules metafield, trimmed to the fields you need. Its productData is a JSON string:
{ "redeemRuleId": 10058, "variantId": 15386745176368 } returned a reward described as A free Ethiopia Coffee, with variantId 15386745176368.
For a
FREE_PRODUCT rule, send the product the customer picked as variantId. The rule’s productData is a JSON string listing the products on the rule: parse it, and send the id of the chosen entry (a product ID for a whole product, a variant ID for a single variant). Without variantId, the reward is for the first entry and its description doesn’t name the product. An id that isn’t on the rule returns 400 with the title Product isn't eligible for discount.customer-loyalty before you retry, to see whether the reward was created. A customer the merchant has excluded, or who opted out, gets 200 and no new reward.
A refused redeem returns 400, with the reason in title. A rule that doesn’t exist or isn’t active, or a call with no redeemRuleId, returns:
title values a customer can meet:
Some titles aren’t written for customers, so show your own message for the ones you expect, and fall back to
title for the rest.
To apply the code to the customer’s cart, request Shopify’s discount link, /discount/{discountCode}, on the store’s domain. The Appstle widget does this with a background fetch. You can also send the customer to that link, and Shopify applies the code before showing the store.
Return a reward
POST /apps/loyalty/cp/api/return-reward/{rewardId} cancels an unused reward and gives its points back. Use a reward’s id from rewards. The store must have Let customers return unused codes for their points turned on, under Points program > Settings > Returning rewards in the Loyalty app.
Only a discount code the customer bought with points can be returned. Show a return button only for a reward that passes the same check as the Appstle widget:
statusisUNUSED, andusageCountis empty or0- it has a
discountCodeand apointTransactionId rewardTypeisn’tSTORE_CREDITexpireDateis empty or in the future
200 with the updated customer-loyalty body: on the demo store availablePoints went from 5536.93 back to 6036.93. A second call for the same reward returns 400:
400. Show the response’s title to the customer, and use errorKey to decide what your UI does next (the reference lists the same keys):
List point transactions
GET /apps/loyalty/cp/api/transaction-by-shop returns the customer’s point transactions, newest first. Use page (zero-based) and size. Each item is one transaction:
The response is an array. The total number of transactions is in the
X-Total-Count response header, so you can compute the number of pages. The response also has a Link header, but its URLs start with http:// and carry a signed proxy query string, so don’t follow them: request the next page with your own page value.
List referrals
GET /apps/loyalty/cp/api/customer-referrals returns the referrals the customer has made, with the same page, size and X-Total-Count behavior. Without sort the order isn’t defined. Add sort=createAt,desc for the newest first, or sort=createAt,asc for the oldest first. Sorting by a property that doesn’t exist returns 500. The friend’s email comes back masked.
status is PENDING (the friend used the link, and the referral rewards haven’t been given yet), CLAIMED (the rewards were given) or RECURRING. For a PENDING referral, referralNote holds the store’s Note on pending referrals, for example You'll get your reward once your friend places an order. If the store has translated the note, add locale, for example locale=fr, to get that translation.
A customer’s own referral link is referralLink in the customer-loyalty response. The reference also lists POST /apps/loyalty/cp/api/add-customer-referral-url for generating it. Call it when referralLink is empty. It always returns 200 with an empty body, so read referralLink from customer-loyalty afterwards.
Enroll a customer
When the store lets customers join the program themselves (theallow_customer_opt_in shop metafield is true), a customer isn’t a member until they join. Until then, customer-loyalty returns 400 with the title Customer not found using this customerID, and the customer has no appstle_loyalty.customer_loyalty metafield. Show a join button to a signed-in customer in that state. The Appstle widget checks the metafield in Liquid, which costs no API call.
POST /apps/loyalty/cp/api/enable-loyalty-program adds the signed-in customer to the program. It takes no body. This is the call the Appstle widget makes.
200 with the plain-text body Customer added successfully. (the Content-Type says JSON, but the body is text). For a customer who is already a member, it returns the same 200 and doesn’t add them again. If it fails, it returns 500 with Shopify’s HTML error page as the body, so the helper returns ok: false. Let the customer try again.
A new member is added in the background, so customer-loyalty keeps returning 400 until that finishes. The Appstle widget waits up to 30 seconds, then reloads the page. If you check sooner, check only a few times, several seconds apart: each 400 counts toward the endpoint’s error limit for the whole store (see Rate limits).
The reference also lists POST /apps/loyalty/cp/api/enroll-customer. It returns 200 even when enrollment fails, so use enable-loyalty-program instead.
You can’t set a VIP tier from the storefront: a vipTier in the enroll-customer body returns 400 with the title VIP tier changes require merchant access. Tiers come from the store’s tier rules, from the merchant, or from the Shopify Flow Assign VIP Tier action (see Shopify Flow).
Other calls
These Storefront API calls cover the rest of what the widget does. Each one takes the signed-in customer’s session and theX-Requested-With header, the same as the calls above.
JavaScript events
Loyalty fires one JavaScript event,AppstleLoyalty:AppliedDiscountCode. The Appstle app embed sends it on document and window when a customer applies one of their existing rewards to the cart from the loyalty widget, with detail set to { discountCode, description, customerId }. It doesn’t fire when the widget applies a new reward’s code automatically right after a redeem. It exists only for the widget: a custom frontend that calls the Storefront API itself never receives it, so update your own UI after your own calls return.
Errors
Further reading
Storefront API reference
Every Storefront API call, with its request and response schema.
Metafields and tags
The rules, settings and customer data the app publishes to your theme.
Authentication
Admin API keys and how storefront calls are authenticated.
Integration guide
Server-side Admin API integration, including headless storefronts.