Generating a token
Session tokens are signed with your site’s SDK token, which comes as a pair: a Key ID and a secret. Find both in LoyaltyLion under Settings → General, in the “SDK secret token” section, where the secret is labelled “Token” (not the site “Token and secret” section above it, which is a different credential and can’t sign session tokens). On Shopify, the pair is also published to your shop as metafields:loyaltylion.secret_id (Key ID) and
loyaltylion.secret_key (secret).
A session token is a base64-encoded JSON payload, a dot, and the hex
HMAC-SHA256 signature of that encoded payload:
On Shopify with Liquid, no backend needed
On a regular Shopify storefront, Liquid’s rendering step is the server-side step: it reads the SDK token from the shop metafields and emits only the finished session token. Add this to your theme layout or loyalty template, replacing123456 with your LoyaltyLion site ID:
- Keep the whitespace control (
{%-/-%}) exactly as shown: a newline captured into the secret corrupts it. customer.idis quoted becausecustomer_idmust be a string, and| jsonon the email escapes anything that would break the JSON.1209600is the 14-day Shopify cap (see Lifetime); a shorter lifetime risks logging shoppers out on cached pages.
window.loyaltylionSessionToken from your storefront JavaScript.
If your store runs the LoyaltyLion JS SDK, your storefront
JavaScript can use the same token for its own Headless API calls.
In a mobile app
Have your backend authenticate the shopper and return a freshly minted session token on every launch and every login, and again ontoken_expired. Don’t bake a token into a build or store one across
sessions: it names one customer and stops working within days.
The payload
Every field is required and unknown fields are rejected, so a typo fails the
token the first time you test, not months later.
Scopes
There are two scopes, and they are independent:write does not include
read, so a token that both reads loyalty state and performs actions must list
both.
Grant only the scopes the client needs: a points-balance widget should be
issued
["read"], so a copied token can’t redeem.
Which endpoints accept a session token
Initialize Session is the one endpoint that creates the customer when
LoyaltyLion doesn’t hold them yet, so a storefront can run entirely on session
tokens. The customer is created from the token’s signed claims, never the
request body. The body’s
customer.email must still match the signed email
(case-insensitively) or the request fails with email_mismatch.
Everything else needs an API key and stays server-side: completing a custom
rule, the referrals endpoints, and every Admin API (/v2/*) endpoint. A
session token sent to any of those is rejected with a 401 that says Session tokens are not supported on this route. On Redeem a custom reward, the
fulfill_immediately and usage options are merchant-side controls and are
rejected with a 403 under a session token.
Moving a browser call off the Admin API
A session token can’t replace an Admin API (/v2/*) call; those stay
API-key-only. If your storefront calls one directly today, there are two routes
off it.
The Headless API already has it. One Get Customer call covers most of what a
storefront reads per shopper, so several /v2/* calls usually collapse into it:
There is no per-customer read on the Admin API, so a storefront showing one
shopper their points is paging the whole customer list to find them. Headless
Get Customer replaces that outright: the session token already names the
customer in its signed claims, so there is no list to search and no way to ask
for anyone else.
history is not the transactions list under another name. A single history
action can cover several transactions: points added and later voided are one
entry whose state changes, not two rows. If your page renders a transaction
ledger, check it against history before switching.
Writes have equivalents too: claiming a reward is
POST /{site_id}/rewards/{type}/redeem under write scope.
Nothing equivalent exists. Then the call belongs on your server with an API
key, and your storefront asks your server rather than us. POST /v2/activities
is the usual case, and deliberately so: an activity a shopper’s own browser can
submit is an activity a shopper can fabricate.
Lifetime
exp is required; there is no default lifetime. We clamp rather than reject:
a token stops working at its own exp or iat plus the cap, whichever comes
first.
The cap follows the site the token is for, not where it is minted: a
backend-minted token for a Shopify site still gets the 14-day cap.
expmust be later thaniat.iatmore than 5 minutes in the future invalidates the token. If one server’s tokens are rejected while others work, check its clock.
Using a token
Send the token as a bearer token on any endpoint that accepts one:channel (and language, if you use it) as query
parameters, as here: the CORS preflight doesn’t allow the equivalent
X-LoyaltyLion-* headers on a cross-origin request.
The token has to agree with the request: the site_id in the path must be the
one it was signed with, and any customer the request names (merchant_id in
the path, or customer_merchant_id in the body) must be its customer.
Requests are rate limited per customer, so one shopper can’t exhaust your
site’s budget, and CORS preflights are answered with
Access-Control-Allow-Origin: *, so a browser can call the API directly.
Errors
Rejections are almost always a401. A 403 means something narrower: the
token is fine, but doesn’t authorize this particular request; see
the 403s.
Two rejections carry a machine-readable code your client should branch on:
token_expired by getting a fresh token and retrying once. Don’t retry
on insufficient_scope: the fix is in the code that issues the token.
Everything else is a 401 with no code:
The signature is checked before the timestamps, so a token that is both
mis-signed and expired reports
INVALID_SIGNATURE, not token_expired.
Some 401s carry no tag. The two worth knowing: a site in the request path
that doesn’t exist (deliberately indistinguishable from an invalid token, so
site IDs can’t be enumerated), and a verified token naming a customer
LoyaltyLion doesn’t hold. Only Initialize Session creates customers, so
everywhere else the signed customer_id must be a customer we already hold.
A token sent to a route that doesn’t accept one is also an untagged 401; see
which endpoints accept a session
token.
403s
A403 always means the token verified. There are four:
insufficient_scope, above.- The request names a different customer than the token was signed for.
- Redeem a custom reward was called with
fulfill_immediatelyorusage. - The site has been uninstalled, or its LoyaltyLion subscription is inactive.