Migrate to expiring offline access tokens
Public apps must use expiring offline access tokens for GraphQL Admin API and REST Admin API requests. New public apps already can't use non-expiring tokens. Shopify enforces the requirement on this timeline:
- January 1, 2027: Public apps that still use non-expiring offline access tokens are 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.
- April 1, 2027: Shopify rejects a GraphQL Admin API or REST Admin API request that presents a non-expiring offline token, and merchants lose access to your app until they re-authenticate.
None of this timeline applies to custom apps, apps created by merchants, or Payments Apps API requests.
You need this page only if your public app still requests non-expiring offline access tokens for the GraphQL Admin API or REST Admin API. Newly scaffolded apps already request expiring tokens through the expiringOfflineAccessTokens flag. This page compares the three ways to stop using non-expiring tokens, and shows how to use each one.
Anchor to Choose a migration pathChoose a migration path
You can stop using non-expiring offline access tokens in three ways. Choose the option that fits how your app gets and uses tokens:
| Option | What your app changes | What happens to existing installations | Choose it when |
|---|---|---|---|
| Option 1: Migrate gradually | Adds expiring=1 to the token requests that it already makes, then stores and uses the refresh token that Shopify returns. | Each installation moves to an expiring token the next time your app gets a new token for that store, such as when a merchant opens your embedded app. | Merchants open your app regularly, and you want the smallest code change. |
| Option 2: Migrate all installations at once | Exchanges each stored non-expiring token for an expiring token pair through token exchange, with no user session. | Each installation that you exchange moves to an expiring token right away. | You don't want to wait for merchants to open your app, or your app runs only in the background. |
| Option 3: Use application-only access tokens | Signs a client assertion with a private key, and uses the client credentials grant to get a short-lived token for any store that has installed your app. | Your app doesn't exchange the stored tokens. It gets a new token for a store when it needs one, and stops sending the stored non-expiring token. | You don't want to store and rotate a refresh token for each store. |
Options 1 and 2 work together. Option 2 replaces the tokens that your app already stores, and option 1 makes sure that the tokens your app gets later also expire. Make the option 1 change before you run option 2.
Application-only access tokens from option 3 also expire. Each one is valid for up to one hour, and Shopify doesn't return a refresh token with it, so your app requests a new token instead of refreshing one.
After you migrate, check for remaining non-expiring tokens.
Anchor to Option 1: Migrate graduallyOption 1: Migrate gradually
Start requesting expiring tokens in the token requests that your app already makes. Each installation moves to an expiring token pair the next time your app gets a new token for that store. Stores where no merchant opens or re-authorizes your app keep their non-expiring tokens until you migrate them all at once.
Find the section below that matches how your app requests tokens today.
Anchor to Apps built from an app templateApps built from an app template
If your app uses @shopify/shopify-app-remix or @shopify/shopify-app-react-router, add the expiringOfflineAccessTokens flag to your shopifyApp() configuration and redeploy:
The app template handles everything else. New token exchanges issue expiring tokens, and the template stores and refreshes them automatically.
Anchor to Apps using token exchange directlyApps using token exchange directly
If your app implements token exchange itself, add expiring=1 to your request:
Store the access_token and refresh_token from the response, along with expires_in and refresh_token_expires_in so you know when each expires. Use the refresh token to get a new access token before expires_in seconds elapse, then store the refresh token that comes back and use that one next time. See Refresh an expiring offline token for the refresh flow.
If your app uses the OAuth authorization code grant, add expiring=1 to the request that exchanges the authorization code for an access token:
Store the access_token and refresh_token from the response, along with expires_in and refresh_token_expires_in. Use the refresh token before expires_in seconds elapse.
Anchor to Option 2: Migrate all installations at onceOption 2: Migrate all installations at once
Exchange each stored non-expiring token for an expiring token pair if you don't want to wait for merchants to open your app and trigger a new token acquisition. This option also covers background-only apps, such as webhook consumers, app proxy backends, and Flow actions.
The migration request is a token exchange that uses the stored non-expiring token as the subject token, together with your app's client ID and client secret. It doesn't require an ID token or a user session. Make the option 1 change first, so that the tokens your app gets later also expire.
Migration permanently invalidates the old token for GraphQL Admin API and REST Admin API requests. Store the access token and refresh token returned by the exchange.
Migration permanently invalidates the old token for GraphQL Admin API and REST Admin API requests. Store the access token and refresh token returned by the exchange.
The response has the same shape as any expiring offline token response. Store the access_token, refresh_token, expires_in, and refresh_token_expires_in, then discard the old token.
For the initial exchange, subject_token must be an active non-expiring offline access token that belongs to your app and store. Shopify rejects online tokens, delegate tokens, scope-restricted tokens, and tokens that already expire. Both requested_token_type and expiring=1 are required. When a parameter doesn't meet these rules, Shopify returns 400 Bad Request with {"error": "invalid_subject_token"} or {"error": "invalid_requested_token_type"}.
Anchor to Recover a lost migration responseRecover a lost migration response
To recover a lost response, repeat the same migration request with the original non-expiring token and valid client credentials within seven days of the initial exchange. Eligible retries return the same token pair. Shopify extends the access token's expiry when needed, but doesn't extend the refresh token's expiry. Save the returned pair and expiry values.
Recovery stops after your app refreshes the issued pair or a later token acquisition for the same store retires it. If a retry returns invalid_subject_token, then acquire a new token through ID-token exchange or the authorization code grant.
Anchor to Option 3: Use application-only access tokensOption 3: Use application-only access tokens
Your server can get an application-only access token for any store that has installed your app, without a user session. Instead of your client secret, it sends a client assertion that it signs with a private key. Use these tokens for the requests that your app makes with stored offline tokens today, such as scheduled syncs and webhook follow-up jobs.
To move your app to application-only access tokens:
- Set up your app to get application-only access tokens, as Get application-only access tokens for installed stores describes. That guide covers the key pair, the client assertion, and the token request.
- Send each application-only access token in the
X-Shopify-Access-Tokenheader, in place of the stored non-expiring token. Request a new token beforeexpires_inseconds elapse. - After your app uses application-only access tokens for every store, stop sending the stored non-expiring tokens. This option doesn't invalidate them, so check for remaining non-expiring tokens to find code that still sends one.
If your app still gets offline access tokens in another flow, such as token exchange when a merchant opens your app, then add expiring=1 to those requests as option 1 describes.
Anchor to Check for remaining non-expiring tokensCheck for remaining non-expiring tokens
Open your app's Logs in the Dev Dashboard, filter Type to GraphQL request, and then filter Token type to Offline, non-expiring. If your app also makes REST Admin API requests, then repeat the check with Type set to REST request. If any requests appear, then your app still sends non-expiring tokens. Migrate the remaining installations with one of the options above.
Anchor to Handle errors during the transitionHandle errors during the transition
Existing installations migrate when your app stores an expiring token pair for them, through option 1 or option 2, or when your app replaces their stored tokens with application-only access tokens.
When Shopify rejects a GraphQL Admin API or REST Admin API request that uses a non-expiring offline token, it returns 403 Forbidden. The response's errors value contains Non-expiring access tokens are no longer accepted for the Admin API, followed by a migration link, so match on that phrase rather than the whole string. A missing access scope also returns 403, so check the message before you re-authenticate.
Handle the rejection according to how your app authenticates:
- Apps built from an app template:
authenticate.admin(request)handles re-authentication automatically. - Apps using token exchange directly: catch the
403and re-run token exchange to get a fresh token. - Apps using the authorization code grant: catch the
403, clear the stored token, and redirect the merchant through the OAuth flow to get a new access token. - Apps using application-only access tokens: the rejection means that some code still sends a stored non-expiring token. Replace that token with an application-only access token.
- Refresh token errors: if a refresh token is expired, already replaced by one your app has since used, or otherwise invalid, Shopify returns
401 Unauthorizedwith{"error": "invalid_request"}. Treat it as a signal to re-authenticate: the merchant must reopen your app in the Shopify admin to trigger a new token request. For the full retry and error behavior, see Refresh an expiring offline token.
Anchor to Next stepsNext steps
- Learn how access tokens work, including the difference between offline and online access modes.
- Set up token refresh so expiring tokens renew before they lapse.
- Review how to rotate your client secret, which also requires replacing stored tokens.