Overview
The Joy Headless API lets headless Shopify storefronts — Hydrogen, Weaverse, mobile apps, and custom frontends — show and run the Joy loyalty program without Liquid. It exposes public, cacheable storefront data (programs, tiers, shop config) and personalised customer data (points, rewards, redeem, earn, referrals) behind a short-lived customer token.
API base URL
https://api.joy.so/app/api/v1/headlessPlan requirements
The Headless API requires the Joy Ultimate plan. Every endpoint answers 403 PLAN_NOT_ELIGIBLE for a recognised shop on any other plan.
Packages
Most storefronts never call the API directly. Pick the package that matches your stack:
@joy-loyalty/sdk — An isomorphic TypeScript client for the Headless API (ESM, web-standard fetch only), so it runs in browsers, Oxygen workers and React Native. @joy-loyalty/sdk/server gives public, shop-scoped calls for server-side loaders; @joy-loyalty/sdk/client gives Bearer-token /customer/* calls. Use it for custom frontends, mobile apps, or any non-Hydrogen stack.
@joy-loyalty/hydrogen — Joy for Hydrogen storefronts. It embeds the Joy Loyalty Widget (<JoyWidget/>) and the Loyalty Page (<JoyLoyaltyPage/>), and ships the server helpers and resource routes they need: the token route, root-loader initial data, and cart wiring. Use it for any Hydrogen storefront without Weaverse.
@joy-loyalty/weaverse-hydrogen — Joy blocks for Weaverse Hydrogen storefronts. It adds a Joy — Loyalty Page block to Weaverse Studio and is built on @joy-loyalty/hydrogen and @joy-loyalty/sdk. Use it when the storefront is built with Weaverse.
npm install @joy-loyalty/hydrogen @joy-loyalty/sdk # Hydrogen
npm install @joy-loyalty/weaverse-hydrogen # add on Weaverse storefrontsHow it works
- Your server proves who the logged-in customer is and exchanges that proof for a customer JWT (
POST /auth/token). See Authentication. - Public endpoints (
/programs,/tiers,/shop-config, ...) need no token and are CDN-cacheable — call them from server-side loaders. Identify the shop with?shop=acme.myshopify.com(?d=is accepted as an alias). - Customer endpoints (
/customer/*) needAuthorization: Bearer <token>.
All responses use the same envelope as the REST API v2:
{ "success": true, "data": {}, "timestamp": "2026-07-31T07:27:54.123Z" }{
"success": false,
"error": { "message": "Invalid or expired token", "code": "UNAUTHORIZED", "statusCode": 401 },
"timestamp": "2026-07-31T07:27:54.123Z"
}GET /info is the one exception — see its entry in the API references.
CORS
The API accepts requests from any origin. It never uses cookies: customer calls authenticate with the Authorization: Bearer header only, so you can call /customer/* directly from the browser.
Caching
| Endpoint | Cache-Control |
|---|---|
/programs, /programs/{id}/place-order-scope, /tiers, /shop-config, /loyalty-page/tier-rewards, guest /info | public, s-maxage=300 |
/loyalty-page/structure | public, s-maxage=30, stale-while-revalidate=300 (a publish reaches storefronts within about 30 seconds) |
/info with a Bearer token or a referral code, every /customer/* route | private, no-store |
Error responses are never cached. Responses are compressed (Brotli or gzip) when the client accepts it.
Rate limits
Every endpoint is rate-limited. When a limit is hit, the API answers 429 RATE_LIMITED with a Retry-After header (seconds) — wait that long, then retry.
| Route | Limit |
|---|---|
| Public routes | 300 requests/min per client IP, or 3000 requests/min per shop for server-signed requests |
POST /auth/token | 30/min per grant identity, 1000 issued tokens/min per shop, 60 failed proofs/min per IP |
POST /referral/generate-link | 10/min per email, 300/min per shop, at most 5 new guest customers per IP per hour |
/customer/* and authenticated /info | 300/min per customer |
On Oxygen and other serverless hosts, every shopper's server-side request leaves from the same egress IP, so unsigned loader calls share one 300/min bucket for the whole store. Sign public calls made from your server — see Server-signed requests.
Localisation
Pass the shopper's language explicitly — the API never reads Accept-Language or the IP address.
- Public and customer endpoints take
locale(for examplefr,zh-CN). On Hydrogen, pass the locale of the current route. GET /infotakesl(locale) anddl(detected buyer locale, which takes precedence).- A language the merchant has not enabled falls back to the shop's primary language. Omit the parameter for the primary language.
There is no country or market parameter.
Pagination
/customer/activitiesand/customer/rewards(and the Loyalty Page lists) takebefore/after(an item id from the current page) andlimit, and returnmeta.paginationwithhasNext/hasPre./customer/store-credit/transactionsuses Shopify cursor pagination: passafterwith the previous page'smeta.pagination.endCursor.
Each endpoint's parameters are listed in the API references.
Install guide
For step-by-step setup of a Hydrogen or Weaverse storefront (environment variables, token route, widget, Loyalty Page), see the merchant install guide: Headless settings (opens in a new tab).