Type: Developer Guide / Fix · Stage: Scale · Topic: App Authentication / Admin API / Migration
Quick answer
Public Shopify apps using non-expiring offline tokens for the GraphQL Admin API must migrate before January 1, 2027. Turn on expiring offline tokens, persist the refresh token and expiry metadata, and migrate every installed shop—including shops that never open your embedded app. Merchants generally do not need to reinstall. This requirement does not apply to merchant-created custom apps.
Do not confuse a one-hour access-token lifetime with a one-hour merchant session. An expiring offline token can be refreshed server-side, including by webhook workers and scheduled jobs.
On this page
Why the old token advice is now wrong
Older OAuth tutorials often say that an offline token is obtained once and stored indefinitely. That was a reasonable description of the older non-expiring model. Shopify introduced expiring offline tokens in December 2025, required them for new public apps beginning April 1, 2026, and extended the requirement to existing public apps effective January 1, 2027.
Shopify’s current migration documentation specifically describes enforcement on GraphQL Admin API calls. Its broader announcement describes Admin API usage generally; for implementation details, follow the narrower, current migration guide rather than assuming a different REST-only integration is exempt from every future change. The safest approach is to migrate affected public apps now.
Find every place your app uses an offline token
- Identify the app’s distribution type. Public app and merchant-created custom app are not interchangeable.
- Inventory token storage per
shop: database session rows, encrypted KV, worker secrets, and any legacy cache. - List every Admin API caller: interactive routes, app proxies, scheduled inventory syncs, webhook consumers, Flow endpoints, and retry workers.
- Check whether tokens have
expires_in,refresh_token, andrefresh_token_expires_inmetadata. An old row containing onlyaccessTokenneeds attention. - Check API Health in the Dev Dashboard for non-expiring-token use, then test the actual token acquisition path. A healthy homepage does not prove background jobs are ready.
For official app templates, enable the migration flag
Shopify’s Remix and React Router app templates support automatic refresh. In shopify.server.ts, enable the flag and redeploy the app:
const shopify = shopifyApp({
// existing app configuration...
future: {
expiringOfflineAccessTokens: true,
},
});
Then deploy using your normal app release process. Make sure the session storage adapter and schema can persist refresh-token and expiry fields. A flag without compatible storage can turn a successful token exchange into a failure on the next refresh.
For custom OAuth code, request and rotate a token pair
If you own the token-exchange code, add expiring=1 to the supported token exchange or authorization-code exchange request. Store both returned credentials and their expiry metadata. Refresh before expires_in elapses, and replace the stored refresh token with the one returned by each refresh. Do not hard-code 3,600 seconds even though that is the current access-token lifetime.
For shops that may never launch the app UI, Shopify provides a direct migration flow using the existing non-expiring offline token and app credentials. The key request fields are:
POST /admin/oauth/access_token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&subject_token=OLD_NON_EXPIRING_OFFLINE_TOKEN
&subject_token_type=urn:shopify:params:oauth:token-type:offline-access-token
&requested_token_type=urn:shopify:params:oauth:token-type:offline-access-token
&expiring=1
Run this server-side; never expose tokens in a browser, article example, or log. The old token is invalidated for GraphQL Admin API use by migration. Persist the new pair before marking that shop migrated. Shopify documents a limited recovery path for a lost migration response, but that is not a substitute for durable storage.
Make refresh safe under concurrency
A webhook, cron job, and admin page can all wake up for the same shop at once. If each refreshes independently, one request can retire credentials another worker just saved. Serialize token acquisition/refresh per shop, use a short lock or compare-and-swap on the token row, and make every worker re-read the latest stored credentials before retrying.
Do not assume every 403 means the token expired. Shopify distinguishes the non-expiring-token rejection message from missing access scopes. Likewise, a refresh 401 can mean the refresh token is invalid or already superseded. Log the class of error without logging token values, and provide a re-authentication path when recovery is impossible.
Roll out store by store
- Back up token-store metadata securely and confirm your encryption/decryption path.
- Deploy refresh-capable code before migrating production installations.
- Migrate one test shop, then exercise a GraphQL Admin query and a background webhook path.
- Verify the stored access token changes on refresh and the latest refresh token is persisted.
- Batch the remaining shops with per-shop success/failure receipts; retry transient failures without overwriting newer credentials.
- Monitor API Health and authentication failures after the migration, including shops with no active staff session.
Common misunderstanding
“Offline means permanent.” Offline means the app can act without a currently signed-in staff member; it does not guarantee a non-expiring credential. “The merchant must reinstall.” Not for the supported server-side migration flow. “My app works in the admin, so my webhooks are safe.” A background worker can still be using a stale token row.
How to test this
- Use a development installation of the same public app and confirm a token exchange returns both access and refresh credentials.
- Make an authenticated GraphQL Admin request immediately after migration.
- Trigger a refresh with a controlled short expiry/test clock or wait for the returned expiry, and confirm the next request succeeds without staff interaction.
- Start two concurrent jobs for one shop; verify only the current token pair wins and neither job permanently breaks the shop.
- Simulate a failed database write after token exchange; verify recovery or explicit re-authentication handling.
- Test a webhook worker and a scheduled task with no browser session.
- Distinguish missing-scope
403from the documented non-expiring-token rejection; confirm secrets never appear in logs.
Sources and further reading
Documentation checked October 23, 2026.

