Update, November 9, 2026: Shopify replaced the enforcement schedule in this article. Its October 27, 2026 notice, updated November 4, says public apps still on non-expiring offline tokens are delisted and blocked from new installs on January 1, 2027, merchants see admin warnings from January 15, 2027, and affected Admin API requests return HTTP 403 from April 1, 2027.[1] The original version, based on the May 20, 2026 notice,[5] said apps would hit authentication errors on January 1. This revision also corrects the refresh-token behavior, adds Shopify's three migration options, and adds client credentials access.[2][3]
If your public Shopify app still uses non-expiring offline access tokens, migrate before January 1, 2027. That date does not switch your app off. It removes the app from the Shopify App Store and blocks new installs, while existing installs keep working. On April 1, 2027, Admin API requests that still present a non-expiring offline token are rejected with HTTP 403. April 1 is the cutoff for remaining requests, not a later deadline.[1][2]
The enforcement timeline
| Date | What happens to a public app still using non-expiring offline tokens |
|---|---|
| April 1, 2026 | Already in effect: public apps created on or after this date must use expiring offline tokens.[6] |
| January 1, 2027 | Delisted from the Shopify App Store and blocked from new installs. Existing installs keep working. |
| January 15, 2027 | Merchants using the app see a warning in the Shopify admin about the risk of service interruption. |
| April 1, 2027 | GraphQL Admin API and REST Admin API requests that present a non-expiring offline token return 403 Forbidden. Merchants lose access to the app until they re-authenticate. |
Sources: Shopify's October 27 changelog notice and migration guide.[1][2]
Who is affected
In scope: public apps that call the GraphQL Admin API or REST Admin API with non-expiring offline access tokens, including apps created before April 1, 2026.[1]
Out of scope: custom apps, apps created by merchants, and Payments Apps API requests.[2]
How expiring offline tokens work
Offline tokens are what an app uses for background work: scheduled syncs, webhook processing and other jobs that run without a merchant in the browser. Shopify's current access token documentation sets these lifetimes:[3]
| Token | Lifetime |
|---|---|
| Expiring offline access token | 1 hour (expires_in is 3600) |
| Refresh token | 90 days when issued (refresh_token_expires_in is 7776000); refreshing can shorten it |
| Non-expiring offline access token | No expiry, until the app is uninstalled or its client secret is revoked |
Shopify recommends reading expires_in from each response instead of hard-coding a duration.[3]
Two rules matter for storage design:
- One current token per app and store. When your app gets a new expiring offline token through token exchange or the authorization code grant, older expiring tokens for that store are retired. A retired access token stays valid until its
expires_inends, so in-flight requests can finish.[3] - Refresh tokens rotate. Every refresh returns a new access token and a new refresh token. The refresh token you presented stays usable until the earliest of: your app uses the newer refresh token, your app acquires a new token, 30 days after it was first used to refresh, or its original 90-day expiry.[3] Always store and use the newest pair.
Do not acquire a new token and refresh an existing one for the same store at the same time. Each retires the other's result, so one of the two tokens is already invalid when it arrives.[3]
Three ways to migrate
Shopify's migration guide offers three options.[2]
Option 1: Migrate gradually
Request expiring tokens in the token requests your app already makes. Each installation moves over the next time your app gets a new token for that store, such as when a merchant opens your embedded app.
Apps built with @shopify/shopify-app-remix or @shopify/shopify-app-react-router set a flag and redeploy:
// shopify.server.tsconst shopify = shopifyApp({// ...future: {expiringOfflineAccessTokens: true,},});
The template stores and refreshes the tokens. Apps that implement token exchange or the authorization code grant themselves add expiring=1 to the token request, then store access_token, refresh_token, expires_in and refresh_token_expires_in.[2]
Stores where no merchant opens or re-authorizes your app keep their non-expiring tokens, so most apps need option 2 or 3 as well.
Option 2: Migrate all installations at once
Exchange each stored non-expiring token for an expiring pair, with no merchant session. This also covers background-only apps such as webhook consumers and Flow actions. Make the option 1 change first.
curl -X POST \https://{shop}.myshopify.com/admin/oauth/access_token \-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \-d 'client_id={client_id}' \-d 'client_secret={client_secret}' \-d 'subject_token={non_expiring_offline_token}' \-d 'subject_token_type=urn:shopify:params:oauth:token-type:offline-access-token' \-d 'requested_token_type=urn:shopify:params:oauth:token-type:offline-access-token' \-d 'expiring=1'
The exchange permanently invalidates the old token for Admin API requests, so store the returned pair before you discard anything. If a response is lost, repeating the same request with the original token within seven days returns the same pair, until your app refreshes it or a later token acquisition retires it.[2]
Option 3: Use application-only access tokens
Shopify's new client credentials access lets your backend get a short-lived token for any store that has installed your app, using a client assertion signed with your app's private key. There is no refresh token to store. For a public app each token lasts up to one hour, and your app requests a new one when it expires.[1][3][4]
This option does not invalidate your stored non-expiring tokens. Your app must stop sending them.[2]
Why Hydrogen teams should check
Hydrogen storefronts mostly call the Storefront API, so the storefront itself is usually unaffected. The risk sits in the services around it: subscription tools, inventory or ERP syncs, review importers and webhook processors. Any of them that is a public app calling the Admin API with a non-expiring offline token is in scope. These background jobs also fail with the least visibility. A sync that starts returning 403 at night can show up days later as wrong inventory.
Migration checklist
- Inventory every Admin API integration. For each one, record whether it is a public app, custom app or merchant-created app, and whether it uses expiring tokens.
- Prioritize public apps on non-expiring tokens. These carry the January 1 listing and new-install penalty and the April 1 request cutoff.
- Choose a migration option. Use option 1 for new tokens, then option 2 or option 3 for installations that merchants do not reopen.
- Centralize token handling. Put acquisition, storage, refresh and retry in one module, and log refresh failures so someone is alerted.
- Handle the error cases. A rejected non-expiring token returns
403 Forbiddenwith anerrorsvalue containingNon-expiring access tokens are no longer accepted for the Admin API. A missing access scope also returns 403, so match on that phrase before you re-authenticate. An expired, replaced or invalid refresh token returns401 Unauthorizedwith{"error": "invalid_request"}, which means the merchant must reopen your app to get a new token.[2] - Verify nothing is left. In your app's Dev Dashboard Logs, filter Type to GraphQL request and Token type to Offline, non-expiring, then repeat with REST request if you use REST. Any request that appears still uses a non-expiring token.[2]
Aim to finish before January 1, 2027. Waiting until April keeps existing installs working, but your app is unlisted, cannot gain new installs, and merchants see warnings in the meantime.
The bottom line
January 1, 2027 is the migration deadline for public apps. Missing it costs your App Store listing and new installs, merchants see warnings from January 15, and remaining Admin API requests with non-expiring tokens fail with 403 from April 1. Custom and merchant-created apps are excluded. Shopify now offers three migration paths, including application-only access tokens that remove the need to store refresh tokens. Related changes to app deployment credentials are covered in our article on App Automation Tokens.
If your team needs help with token migration for public apps or Hydrogen companion services, contact the Weaverse team.
Sources
- Shopify developer changelog: Migrate to expiring offline access tokens before January 1, 2027 (October 27, 2026, updated November 4, 2026)
- Shopify docs: Migrate to expiring offline access tokens
- Shopify docs: Access tokens
- Shopify docs: Get application-only access tokens for installed stores
- Shopify developer changelog: Expiring offline access tokens required for all public apps as of January 1, 2027 (May 20, 2026, superseded schedule)
- Shopify developer changelog: Expiring offline access tokens required for public apps (April 1, 2026)



