From January 1, 2027, every public Shopify app must use expiring offline access tokens when it calls the Admin API. Public apps that still send the old, non-expiring offline tokens will get authentication errors. New public apps have needed expiring tokens since April 1, 2026; the 2027 deadline extends the rule to every public app, including ones created before that date. Custom apps and merchant-created apps are not affected (custom vs public apps explained).
We track this date with the other platform deadlines on our Shopify deprecation tracker. This guide covers what changes, how to migrate an app built on Shopify's templates or on a custom OAuth setup, and the one step most migration checklists miss: stores whose merchants won't open your app before the deadline.
What changes
- Access tokens expire after 1 hour. The token response includes
expires_in: 3600. - Each access token comes with a refresh token. It's valid for 90 days when issued (
refresh_token_expires_in: 7776000). Read the value from the response rather than hard-coding 90 days. - Refresh tokens rotate. Every refresh returns a new access token *and* a new refresh token. Store both, and use the new refresh token next time.
- There's a recovery window. Since August 28, 2026, an app can retry its previously stored refresh token until it starts using the replacement, for up to 30 days after that token was first used. This protects you if a refresh succeeds but your app fails to save the response.
- An invalid refresh token returns
401 Unauthorizedwith{"error": "invalid_request"}. The merchant then has to reopen your app in the Shopify admin to get a new token. - Merchants don't reinstall. Your app exchanges each existing token for an expiring one in code.
Does this apply to your app?
- It's a public app (listed on the App Store or distributed to multiple stores) → yes.
- It stores an offline token and calls the Admin API from background jobs, webhooks, cron syncs or anything that runs without the merchant → yes, and you need the proactive migration below.
- It's a custom app for a single store, or an app created by a merchant in their admin → no.
Path A: apps built on Shopify's app template
If your app uses @shopify/shopify-app-react-router or @shopify/shopify-app-remix, the library already handles the exchange, storage and refresh. You need three things.
- Turn on the future flag in
shopify.server:
const shopify = shopifyApp({
// ...existing config
future: {
expiringOfflineAccessTokens: true,
},
});
- Make sure your session storage can hold the refresh token. The current React Router template's Prisma
Sessionmodel includes two extra columns. If your schema came from an older template, add them and run a migration:
model Session {
// ...existing fields
refreshToken String?
refreshTokenExpires DateTime?
}
- Update the packages. Move
@shopify/shopify-app-*,@shopify/shopify-apiand your session-storage adapter to current versions, then deploy.
Once that's live, new token exchanges return expiring tokens. The template stores and refreshes them automatically. Existing non-expiring tokens are exchanged the next time each merchant opens your app.
Path B: apps with their own OAuth code
If you handle tokens yourself, the exchange is a single POST to the store's token endpoint. expiring: "1" asks for an expiring token:
async function exchangeForExpiringToken(shop, nonExpiringToken) {
const res = await fetch(`https://${shop}/admin/oauth/access_token`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
client_id: process.env.SHOPIFY_API_KEY,
client_secret: process.env.SHOPIFY_API_SECRET,
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token: nonExpiringToken,
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",
}),
});
if (!res.ok) throw new Error(`Token exchange failed for ${shop}: ${res.status}`);
// { access_token, expires_in, refresh_token, refresh_token_expires_in, ... }
return res.json();
}
To refresh, send the refresh token with grant_type: "refresh_token":
async function refreshAccessToken(shop, refreshToken) {
const res = await fetch(`https://${shop}/admin/oauth/access_token`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
client_id: process.env.SHOPIFY_API_KEY,
client_secret: process.env.SHOPIFY_API_SECRET,
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
if (res.status === 401) throw new Error(`Re-authentication needed for ${shop}`);
if (!res.ok) throw new Error(`Refresh failed for ${shop}: ${res.status}`);
return res.json(); // new access_token AND new refresh_token
}
If you use @shopify/shopify-api without the app template, the library exposes both steps: shopify.auth.migrateToExpiringToken({ shop, nonExpiringOfflineAccessToken }) and shopify.auth.refreshToken({ shop, refreshToken }).
The step most checklists miss: stores that never open your app
With the templates, an existing token is only exchanged when the merchant next opens your app. Many merchants never do. A reviews widget, a sync app or an SMS app can run for months on webhooks and scheduled jobs alone. Any store that still holds a non-expiring token on January 1, 2027 breaks the first time a background job calls the Admin API.
So migrate proactively: run a one-off job that exchanges the token for every installed store. Shopify's migration guide flags two rules that shape it:
- The exchange is irreversible and not safe to replay. Shopify destroys the non-expiring token in the same transaction that issues the expiring one. If you lose the response, that store's token is gone and the merchant has to reopen your app.
- Persist the new token pair before you mark a store as done.
In practice, that means:
- Select stores whose stored token has no refresh token.
- For each store, exchange, then write
access_token,refresh_tokenand both expiry times in one database transaction. - Process stores one at a time with a lock per store, so two workers never exchange the same token.
- Log every failure with the shop domain. Don't blind-retry an exchange; check whether the new pair was already saved.
- Re-run for stores that were skipped, then confirm that no non-expiring tokens remain.
Background jobs and webhooks after migration
These are the jobs that break first, because they run with no merchant in the admin. If your webhook workers process deliveries in the background, give them the same token service as everything else.
- Refresh before you call, not after it fails. Check the stored expiry and refresh when the token is close to expiring.
- Allow one refresh per store at a time. If ten webhook jobs for the same store refresh in parallel, they race each other's rotated tokens. A per-store lock, or a single token service that the other jobs call, avoids it.
- Treat
401 invalid_requestas re-authentication. Flag the store, stop retrying, and prompt the merchant to reopen the app, for example by email or an admin notice.
Testing checklist
- Install the app on a development store and confirm the token response includes
refresh_tokenandexpires_in. - Change the stored expiry to force a refresh, and confirm both tokens rotate in your database.
- Run your migration job against a development store that holds an old non-expiring token.
- Trigger your webhooks and scheduled jobs with no merchant session open, and confirm they still reach the Admin API.
- Simulate a lost refresh response and confirm the app recovers with the previous refresh token.
If your app still calls the REST Admin API, plan its move from REST to GraphQL in the same release: both changes touch the same API client.
Want a second pair of hands?
We run this as a fixed-scope compliance sprint for Shopify app teams. It covers a readiness audit of your repository, the template upgrade or custom OAuth changes, the proactive migration job, and a test run on development stores, finished well before January 1, 2027. See our Shopify app development services, or tell us about your app and we'll reply with a fixed quote.
Sources
- Shopify changelog: Expiring offline access tokens required for all public apps as of January 1, 2027
- Shopify: Migrate to expiring offline access tokens
- Shopify: About offline access tokens
- Shopify changelog: More resilient refreshes for expiring offline access tokens
- Shopify changelog: Offline access tokens now support expiry and refresh
- Shopify React Router app template (Prisma schema and shopify.server)



