Headless API
Overview

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/headless

Plan 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 storefronts

How it works

  1. Your server proves who the logged-in customer is and exchanges that proof for a customer JWT (POST /auth/token). See Authentication.
  2. 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).
  3. Customer endpoints (/customer/*) need Authorization: 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

EndpointCache-Control
/programs, /programs/{id}/place-order-scope, /tiers, /shop-config, /loyalty-page/tier-rewards, guest /infopublic, s-maxage=300
/loyalty-page/structurepublic, 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/* routeprivate, 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.

RouteLimit
Public routes300 requests/min per client IP, or 3000 requests/min per shop for server-signed requests
POST /auth/token30/min per grant identity, 1000 issued tokens/min per shop, 60 failed proofs/min per IP
POST /referral/generate-link10/min per email, 300/min per shop, at most 5 new guest customers per IP per hour
/customer/* and authenticated /info300/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 example fr, zh-CN). On Hydrogen, pass the locale of the current route.
  • GET /info takes l (locale) and dl (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/activities and /customer/rewards (and the Loyalty Page lists) take before / after (an item id from the current page) and limit, and return meta.pagination with hasNext / hasPre.
  • /customer/store-credit/transactions uses Shopify cursor pagination: pass after with the previous page's meta.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).