Appearance
Headless Support
Plan requirement: Headless channel access requires the Scale plan.
Headless storefronts should call SaveLayer through direct APIs, not through Shopify app proxy.
Auth model (v1)
- Backend-to-backend exchange: Your storefront server (already holding a valid Customer Account API access token from Shopify’s OAuth flow) calls
POST /api/headless/auth/exchangewith JSON{ "shop": "<myshopify-host>", "customerAccountAccessToken": "<token>" }. - SaveLayer discovers the Customer Account GraphQL endpoint from
https://{shop}/.well-known/customer-account-api, runs acustomer { id }query with that token, verifies install + plan channel, and returnsdata.authorization(Bearer <jwt>) plus metadata. Token TTL is 300 seconds. - Subsequent calls: Send
Authorization: Bearer <SaveLayer JWT>to headless operation routes (for examplePOST /api/headless/save,GET /api/headless/list). Do not send customer id in the payload for authentication; identity comes from the verified JWT only.
INVALID_HEADLESS_SIGNATURE (401) on exchange means SaveLayer could not validate the Customer Account token or resolve a customer (including discovery or GraphQL failures)—not that you configure a separate merchant “signed header” protocol.
Legacy Storefront Token Exchange
For older headless storefronts that still use legacy Storefront API customer access tokens (from customerAccessTokenCreate), the same exchange endpoint accepts an alternative body:
json
{
“shop”: “<myshopify-host>”,
“storefrontCustomerAccessToken”: “<legacy-customer-token>”,
“storefrontAccessToken”: “<shop-public-storefront-api-token>”
}SaveLayer calls the Storefront API customer { id } query using both tokens to resolve the customer, then mints the same SaveLayer JWT. The response shape is identical to the modern path.
storefrontCustomerAccessToken: the customer's legacy access token (fromcustomerAccessTokenCreate)storefrontAccessToken: the shop's public Storefront API access token (the same one used to callcustomerAccessTokenCreate)INVALID_LEGACY_STOREFRONT_TOKEN(401): the legacy token is invalid, expired, or could not resolve a customer
Security note: Legacy Storefront customer tokens can be long-lived (up to 90 days). SaveLayer's minted JWT remains short-lived (300 seconds) regardless of the source token's lifetime. The modern Customer Account API path is preferred for new integrations.
The request body must contain either customerAccountAccessToken (modern) or storefrontCustomerAccessToken + storefrontAccessToken (legacy)—not both.
For the full direct-auth model (namespaces, DIRECT_API_JWT_SECRET, deploy targets), see the marketing site Authorization doc at /docs/authorization.
Why direct APIs are preferred
- app proxy is Shopify-specific and merchant-customizable
- headless clients need a stable integration surface
- a dedicated
/api/headless/*namespace keeps server-to-server exchange and JWT usage explicit
Headless calls are normally server-to-server; you are not expected to rely on browser CORS for the exchange. Run exchange on your backend and keep tokens off the client when possible.
Current channel shape
- direct API namespace for headless clients
- shared request and response contracts with the Online Store channel
- one shared service layer behind all ingress adapters
Configuration
- Local: set
DIRECT_API_JWT_SECRETinapps/pages/.dev.vars(seeapps/pages/.dev.vars.example) and mirror the same value in repo root.envfor Vite’sbuildLoadContextoverlay duringnpm run dev. - Deployed: define GitHub Environment secret
DIRECT_API_JWT_SECRETin bothStagingandProduction(whichever environments your deploy workflow uses). The workflow syncs it to the Cloudflare Pages app project viawrangler pages secret put—not to the Worker.