Authentication
Every call has to answer two questions: which shop, and which customer. You never send an API key — Shopify answers both for you.
There are two supported ways in.
1. Shopify App Proxy — use this from a theme
Call the loyalty app on the shop's own domain:
https://your-shop.myshopify.com/apps/loyalty-proxy/member
From a theme you write it as a relative URL, so it works on the live domain, on preview themes and on development stores alike:
const res = await fetch('/apps/loyalty-proxy/member');
Shopify forwards the call to us, signs it, and appends the id of the customer whose session cookie the browser sent. There is nothing for you to add — no header, no token, no customer id. A request that is not signed by Shopify is rejected.
This is why the proxy path only works from a page served on the shop domain: the customer session lives there.
2. Session token — use this from a Shopify UI extension
Checkout and customer-account UI extensions run in a sandboxed frame and cannot use the App Proxy. They call the API host directly and identify the customer with a Shopify session token:
import {api} from '@shopify/ui-extensions/checkout';
const token = await api.sessionToken.get();
const res = await fetch('https://api.ecommerce.knistrloyalty.cloud/storefront/member', {
headers: {'X-Shopify-Session-Token': token},
});
Note the header name: X-Shopify-Session-Token, not Authorization: Bearer. The customer is read
from the token itself, so a logged_in_customer_id in the query string is ignored on this path.
Tokens are short-lived — fetch a fresh one per call rather than caching it.
Headless storefronts on your own domain
The customer's Shopify session only exists on the shop domain, and Shopify only issues session tokens inside its own extension surfaces. A storefront served from your own domain therefore has neither, and cannot call the Storefront API directly today.
If that is your setup, talk to us before you build — it is on the roadmap and we would rather design it with you than have you work around it.
Responses you must handle
| Status | Means | Do this |
|---|---|---|
401 | No customer is logged in | Send the visitor to /account/login |
404 | Logged in, but not a loyalty member yet | Show your "Join the programme" call to action |
409 | The action was already done (e.g. birthday already set) | Show the existing value, not an error |
400 | The request itself is wrong — a missing optionId, a birthdate that is not --MM-DD, or consentGiven not true | Fix the call; this one is on you, not on the customer |
503 | Shopify did not answer when we tried to read the customer | Ask the customer to try again in a moment |
401 and 404 are normal states of a loyalty UI, not failures — a signed-out visitor and a
not-yet-joined customer are the two most common visitors you will have.
400 and 503 are the two you are most likely to forget. POST /register can return either, so a
join button that assumes success will silently do nothing when one of them comes back. Every error
body is the same shape: {"error": "..."}, in English, meant for your logs rather than for the
customer.
One endpoint is different
GET /config returns the shop's programme configuration — branding, earning rules, reward catalogue.
It is shop-scoped: it needs no logged-in customer and never returns customer data. You can call it to
render the rewards page for signed-out visitors.