Headless API
Auth

Auth

Customer token exchange — turns proven identity into a short-lived JWT

Exchange proven identity for a short-lived customer JWT

POST/auth/token

Two grant_types issue the same JWT — everything downstream is client-agnostic:

  • hmac — web / Weaverse. The merchant server (Oxygen worker) knows the session and holds the shop's Headless secret (Joy admin > Settings > Manage keys) in a private env var. It signs HMAC_SHA256("{customerId}-{lowercased email}-{ts}", headlessSecret) (hex) and sends it here with ts (Unix seconds; rejected if more than 300s off server time). customerId is the numeric Shopify customer id, not the gid://shopify/Customer/... GID. The headless secret never reaches the browser. The shop's REST API secretKey is not accepted.
  • shopify_customer_token — mobile / native. The client sends a Shopify Customer Account API access token; Joy verifies it with Shopify and issues the same JWT. The client never holds the headless secret, and this grant works even before the merchant generates one (Joy creates the shop's headless secret on the first mint if it does not exist yet).

Fail-closed: any invalid/missing proof returns 401 before any work. The customer must already exist in Joy, and for hmac the signed email must match the customer's Joy email (case-insensitive).

The shop must be on the Ultimate plan, otherwise 403 PLAN_NOT_ELIGIBLE.

Request body
Responses
200JWT issued
successbooleanOptional
Example: true
dataTokenDataOptional
Show properties
tokenstringOptional

Signed customer JWT (HS256).

Example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
tokenTypestringOptional
Example: "Bearer"
expiresInintegerOptional

Seconds until expiry.

Example: 3600
metaobjectOptional

Optional metadata (pagination, counts).

timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
400`BAD_REQUEST` - a required field is missing. `UNSUPPORTED_GRANT_TYPE` - `grant_type` is not `hmac` or `shopify_customer_token`.
successbooleanOptional
Example: false
errorobjectOptional
Show properties
messagestringOptional
Example: "Unauthorized"
codestringOptional
Example: "UNAUTHORIZED"
statusCodeintegerOptional
Example: 401
timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
401`UNAUTHORIZED` - identity proof invalid or expired (`ts` more than 300s off), shop not found, customer not found in Joy, or the signed email does not match the customer's Joy email.
successbooleanOptional
Example: false
errorobjectOptional
Show properties
messagestringOptional
Example: "Unauthorized"
codestringOptional
Example: "UNAUTHORIZED"
statusCodeintegerOptional
Example: 401
timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
403`EXCLUDED` - the customer is excluded from the loyalty program (excluded tag / segment), so no token is minted. Do not retry. `PLAN_NOT_ELIGIBLE` - the shop is not on the Ultimate plan.
successbooleanOptional
Example: false
errorobjectOptional
Show properties
messagestringOptional
Example: "Unauthorized"
codestringOptional
Example: "UNAUTHORIZED"
statusCodeintegerOptional
Example: 401
timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
429
No body
500`INTERNAL_ERROR` - any unexpected server failure (every thrown error on the headless surface maps here). Safe to retry with backoff.
successbooleanOptional
Example: false
errorobjectOptional
Show properties
messagestringOptional
Example: "Unauthorized"
codestringOptional
Example: "UNAUTHORIZED"
statusCodeintegerOptional
Example: 401
timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
503`SERVICE_UNAVAILABLE` - the token could not be issued; retry. `RATE_LIMITER_UNAVAILABLE` - the token rate limiter is temporarily unavailable (this endpoint fails closed); retry after `Retry-After` seconds.
successbooleanOptional
Example: false
errorobjectOptional
Show properties
messagestringOptional
Example: "Unauthorized"
codestringOptional
Example: "UNAUTHORIZED"
statusCodeintegerOptional
Example: 401
timestampstring · date-timeOptional
Example: "2026-07-31T07:27:54.123Z"
POST/auth/token
POST /app/api/v1/headless/auth/token HTTP/1.1
Host: api.joy.so
Content-Type: application/json
Accept: */*
Content-Length: 173
{
"grant_type": "hmac",
"shopDomain": "acme.myshopify.com",
"customerId": "6543210987654",
"email": "[email protected]",
"ts": 1753939674,
"hmac": "9f1c2b...e7"
}
200JWT issued
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600
},
"meta": {},
"timestamp": "2026-07-31T07:27:54.123Z"
}