Skip to main content

Migrate to expiring offline access tokens

Public apps must use expiring offline access tokens by January 1, 2027. After that date, Shopify invalidates any remaining non-expiring tokens, and merchants lose access to your app until they re-authenticate. This requirement doesn't apply to custom apps or apps created by merchants.

You need this page only if your public app still requests non-expiring offline access tokens. Newly scaffolded apps already request expiring tokens through the expiringOfflineAccessTokens flag. This page covers how to stop requesting non-expiring tokens and cycle the ones you've already issued.


  1. Stop requesting non-expiring tokens: Update your app's configuration to request expiring tokens with refresh tokens on new token requests.
  2. Cycle existing tokens: Existing non-expiring tokens on installed merchants cycle automatically the next time each merchant opens your app and your app requests a new token. You don't need to prompt merchants or take action per-merchant. To control the timing yourself, or to cover an app that merchants don't open, cycle them with token exchange.

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:

// shopify.server.ts
const shopify = shopifyApp({
// ...
future: {
expiringOfflineAccessTokens: true,
},
});
shopify app deploy

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:

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 'subject_token={id_token}' \
-d 'subject_token_type=urn:ietf:params:oauth:token-type:id_token' \
-d 'client_id={client_id}' \
-d 'client_secret={client_secret}' \
-d 'expiring=1'

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. See Refresh an expiring offline token for the refresh flow.


Anchor to Apps using the authorization code grantApps using the authorization code grant

If your app uses the OAuth authorization code grant, add expiring=1 to the request that exchanges the authorization code for an access token:

curl -X POST \
https://{shop}.myshopify.com/admin/oauth/access_token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id={client_id}' \
-d 'client_secret={client_secret}' \
-d 'code={authorization_code}' \
-d 'expiring=1'

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 Cycle existing tokens without waiting for a merchantCycle existing tokens without waiting for a merchant

Cycling on next app open covers most installs, but it gives you no control over timing, and it never covers an app that merchants don't open. Apps that run entirely in the background, such as webhook consumers, App Proxy backends, and Flow actions, have no app launch to wait for.

You can cycle a non-expiring token directly with token exchange. This call authenticates with the stored token itself, so it needs no ID token and no merchant session, which means you can run it from a background job across your install base.

Caution

Cycling is irreversible, and it isn't safe to replay. Shopify destroys the non-expiring token in the same transaction that issues the expiring one, so if your app loses the response, that store has no usable token and the merchant has to reauthorize before your app can call the GraphQL Admin API again. Persist the new token pair before you mark a store as done.

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 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.

subject_token must be a non-expiring offline access token that belongs to your app and is still active. Online tokens, delegate tokens, scope-restricted tokens, and tokens that already expire are all rejected. 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"}, including for a store you've already cycled.


Anchor to Handle errors during the transitionHandle errors during the transition

After you deploy, existing installs with non-expiring tokens remain valid. They cycle to expiring tokens the next time each merchant opens your app, or when you cycle them yourself. After January 1, 2027, Shopify invalidates any tokens that haven't cycled.

When your app encounters an invalidated token, Shopify returns 401 Unauthorized from the GraphQL Admin API:

  • Apps built from an app template: authenticate.admin(request) handles re-authentication automatically.
  • Apps using token exchange directly: catch the 401 and re-run token exchange to get a fresh token.
  • Apps using the authorization code grant: catch the 401, clear the stored token, and redirect the merchant through the OAuth flow to get a new access token.
  • Refresh token errors: if a refresh token is expired, already used outside the retry window, or otherwise invalid, Shopify returns 401 Unauthorized with {"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.


Was this page helpful?