Overview
Package: @shopify/shopify-app-express@7.0.0
Related package: @shopify/shopify-api@13.0.0
Problem
As of April 1, 2026, Shopify requires expiring offline tokens for all new public apps
(changelog: https://shopify.dev/changelog/expiring-offline-access-tokens-required-for-new-public-apps-as-of-april-1-2026).
However, shopify-app-express never passes expiring: true when calling api.auth.callback()
in auth-callback.ts:
// packages/apps/shopify-app-express/src/auth/auth-callback.ts
const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
// expiring is never passed — defaults to false
});
shopify-api@13 supports this via the expiring parameter (added in December 2025), but
shopify-app-express never uses it, so Shopify always returns a non-expiring shpat_ token.
Any API call made with that token on a new public app returns:
{ "networkStatusCode": 403, "message": "GraphQL Client: Forbidden" }
Expected behaviour
shopify-app-express should pass expiring: true by default for offline token OAuth flows,
since Shopify now mandates expiring tokens for all new public apps. Ideally this should also
be configurable (e.g. a useExpiringTokens option in shopifyApp() config) for apps that
need to opt out or migrate gradually.
Suggested fix
In auth-callback.ts, change:
const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
});
to:
const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
expiring: true,
});
Workaround
Skip shopify.auth.callback() and call shopify.api.auth.callback({ expiring: true })
directly in your own route handler, storing the session and registering webhooks manually.
Overview
Package: @shopify/shopify-app-express@7.0.0
Related package: @shopify/shopify-api@13.0.0
Problem
As of April 1, 2026, Shopify requires expiring offline tokens for all new public apps
(changelog: https://shopify.dev/changelog/expiring-offline-access-tokens-required-for-new-public-apps-as-of-april-1-2026).
However,
shopify-app-expressnever passesexpiring: truewhen callingapi.auth.callback()in
auth-callback.ts:// packages/apps/shopify-app-express/src/auth/auth-callback.ts
const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
// expiring is never passed — defaults to false
});
shopify-api@13supports this via theexpiringparameter (added in December 2025), butshopify-app-expressnever uses it, so Shopify always returns a non-expiringshpat_token.Any API call made with that token on a new public app returns:
{ "networkStatusCode": 403, "message": "GraphQL Client: Forbidden" }
Expected behaviour
shopify-app-expressshould passexpiring: trueby default for offline token OAuth flows,since Shopify now mandates expiring tokens for all new public apps. Ideally this should also
be configurable (e.g. a
useExpiringTokensoption inshopifyApp()config) for apps thatneed to opt out or migrate gradually.
Suggested fix
In
auth-callback.ts, change:const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
});
to:
const callbackResponse = await api.auth.callback({
rawRequest: req,
rawResponse: res,
expiring: true,
});
Workaround
Skip
shopify.auth.callback()and callshopify.api.auth.callback({ expiring: true })directly in your own route handler, storing the session and registering webhooks manually.