---
title: Migrate to expiring offline access tokens
description: >-
  Steps to move your app to expiring offline access tokens before the January 1,
  2027 deadline.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/authentication-authorization/migrate-to-expiring-offline-access-tokens
  md: >-
    https://shopify.dev/docs/apps/build/authentication-authorization/migrate-to-expiring-offline-access-tokens.md
---

# 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](https://shopify.dev/changelog/expiring-offline-access-tokens-required-for-all-public-apps-as-of-january-1-2027), 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.

***

## What you need to do

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](#cycle-existing-tokens-without-waiting-for-a-merchant).

Find the section below that matches how your app requests tokens today.

***

## Apps 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:

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

```bash
shopify app deploy
```

The app template handles everything else. New token exchanges issue expiring tokens, and the template stores and refreshes them automatically.

***

## Apps using token exchange directly

If your app implements token exchange itself, add `expiring=1` to your request:

```terminal
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](https://shopify.dev/docs/apps/build/authentication-authorization/implement-token-exchange#refresh-an-expiring-offline-token) for the refresh flow.

***

## Apps 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:

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

***

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

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

***

## Handle 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](#cycle-existing-tokens-without-waiting-for-a-merchant). 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](https://shopify.dev/docs/apps/build/authentication-authorization/implement-token-exchange#refresh-an-expiring-offline-token).

***

## Next steps

* Learn how [access tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens) work, including the difference between offline and online access modes.
* Set up [token refresh](https://shopify.dev/docs/apps/build/authentication-authorization/implement-token-exchange#refresh-an-expiring-offline-token) so expiring tokens renew before they lapse.
* Review how to [rotate your client secret](https://shopify.dev/docs/apps/build/authentication-authorization/manage-credentials#rotate-your-client-secret), which also requires cycling stored tokens.

***
