Skip to main content
A customer session token authenticates Headless API calls made from a shopper’s own device: storefront JavaScript, a mobile app, a headless frontend. It is a credential for a single customer on a single site. You generate and sign it yourself, with no LoyaltyLion API call involved, and it authorizes only that customer’s own self-serve actions, which is what makes it safe to hand to a client: even from devtools, it only lets the shopper act as themselves. That means you can call the Headless API directly from the client, instead of proxying calls through your backend first.

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).
Treat the secret like a password: anyone holding it can mint a session token for any of your customers. It stays on your server or in your theme; only the finished session token goes to the client.
A session token is a base64-encoded JSON payload, a dot, and the hex HMAC-SHA256 signature of that encoded payload:
The comments in each recipe explain how to fill in every field:
The JSON formatting is yours: we verify the exact encoded string you sign, so key order and whitespace don’t matter, either base64 alphabet works (padded or unpadded), and the hex signature can be upper or lower case.

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, replacing 123456 with your LoyaltyLion site ID:
Three details that matter here:
  • Keep the whitespace control ({%- / -%}) exactly as shown: a newline captured into the secret corrupts it.
  • customer.id is quoted because customer_id must be a string, and | json on the email escapes anything that would break the JSON.
  • 1209600 is the 14-day Shopify cap (see Lifetime); a shorter lifetime risks logging shoppers out on cached pages.
Then use 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

Never embed the SDK token secret in an app build. Anything in the binary can be extracted, and anyone holding the secret can mint a session token for any of your customers.
Have your backend authenticate the shopper and return a freshly minted session token on every launch and every login, and again on token_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.
  • exp must be later than iat.
  • iat more than 5 minutes in the future invalidates the token. If one server’s tokens are rejected while others work, check its clock.
Issue the shortest lifetime that fits: minutes from a backend minting per session; the full 14 days from a Shopify theme, because Shopify serves cached pages (including any token rendered into them) for far longer than a day.

Using a token

Send the token as a bearer token on any endpoint that accepts one:
From a browser, pass 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 a 401. 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:
Handle 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 tag in brackets says which check failed: 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

A 403 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_immediately or usage.
  • The site has been uninstalled, or its LoyaltyLion subscription is inactive.