Web/Guru

Shopify Expiring Offline Access Tokens: What Public Apps Must Change Before January 1, 2027

From January 1, 2027, public Shopify apps must use expiring offline access tokens. How to migrate, and the stores most migration checklists forget.

Published
Updated
October 5, 2026
Read time
8 min read
Written by
Webguru
Shopify Expiring Offline Access Tokens: What Public Apps Must Change Before January 1, 2027
/On this page01 / 09

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 Unauthorized with {"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.

  1. Turn on the future flag in shopify.server:
js
const shopify = shopifyApp({
  // ...existing config
  future: {
    expiringOfflineAccessTokens: true,
  },
});
  1. Make sure your session storage can hold the refresh token. The current React Router template's Prisma Session model includes two extra columns. If your schema came from an older template, add them and run a migration:
prisma
model Session {
  // ...existing fields
  refreshToken        String?
  refreshTokenExpires DateTime?
}
  1. Update the packages. Move @shopify/shopify-app-*, @shopify/shopify-api and 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:

js
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":

js
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:

  1. Select stores whose stored token has no refresh token.
  2. For each store, exchange, then write access_token, refresh_token and both expiry times in one database transaction.
  3. Process stores one at a time with a lock per store, so two workers never exchange the same token.
  4. Log every failure with the shop domain. Don't blind-retry an exchange; check whether the new pair was already saved.
  5. 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_request as 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_token and expires_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

/Tags#Shopify#Apps#Admin API

Written by

Webguru

Shopify Development Team

Webguru.pk is a Shopify development team in Lahore, Pakistan, building stores, custom apps and integrations for brands worldwide.

/Share

Need a hand with this?

We design, build and fix this kind of work for Shopify, SaaS and AI products.

/faqs

Expiring Offline Access Tokens: Frequently Asked Questions

New public apps created on or after April 1, 2026 already need them. From January 1, 2027, every public app must use expiring offline access tokens for Admin API requests, including apps created before April 1, 2026. Apps still using non-expiring tokens after that date receive authentication errors.

No. The requirement applies to public apps. Custom apps and apps created by merchants in their Shopify admin are not affected.

The access token expires after 1 hour (expires_in is 3600). The refresh token is valid for 90 days when issued (refresh_token_expires_in is 7776000). Every refresh returns a new access token and a new refresh token, and your app should use the new refresh token next time.

No. Your app exchanges each existing non-expiring token for an expiring one in code. Apps on Shopify's templates do this automatically the next time the merchant opens the app, so stores whose merchants don't open the app need a proactive migration job.

Shopify returns 401 Unauthorized with {"error": "invalid_request"}. Treat it as a signal to re-authenticate: the merchant has to reopen your app in the Shopify admin so a new token can be requested.

Public app not ready for January 1, 2027?

We audit your token handling, upgrade the template or OAuth code, and migrate every installed store before the deadline.

or write to+44 7882 755642