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.