Skip to content

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) ​

  1. 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/exchange with JSON { "shop": "<myshopify-host>", "customerAccountAccessToken": "<token>" }.
  2. SaveLayer discovers the Customer Account GraphQL endpoint from https://{shop}/.well-known/customer-account-api, runs a customer { id } query with that token, verifies install + plan channel, and returns data.authorization (Bearer <jwt>) plus metadata. Token TTL is 300 seconds.
  3. Subsequent calls: Send Authorization: Bearer <SaveLayer JWT> to headless operation routes (for example POST /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 (from customerAccessTokenCreate)
  • storefrontAccessToken: the shop's public Storefront API access token (the same one used to call customerAccessTokenCreate)
  • 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_SECRET in apps/pages/.dev.vars (see apps/pages/.dev.vars.example) and mirror the same value in repo root .env for Vite’s buildLoadContext overlay during npm run dev.
  • Deployed: define GitHub Environment secret DIRECT_API_JWT_SECRET in both Staging and Production (whichever environments your deploy workflow uses). The workflow syncs it to the Cloudflare Pages app project via wrangler pages secret put—not to the Worker.