---
title: Migrate to expiring offline access tokens
description: >-
  Steps to move your app to expiring offline access tokens for GraphQL Admin API
  requests 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 for [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) requests. New public apps already can't use non-expiring tokens, and existing public apps can't after January 1, 2027. After that date, Shopify [rejects a GraphQL Admin API request that presents a non-expiring offline token](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.

Shopify enforces this on GraphQL Admin API requests only. It 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 for the GraphQL Admin API. Newly scaffolded apps already request expiring tokens through the `expiringOfflineAccessTokens` flag. This page covers how to stop requesting non-expiring tokens and migrate existing installations.

***

## What you need to do

1. **Start requesting expiring tokens**: Enable the [app template flag](#apps-built-from-an-app-template), or include `expiring=1` in your app's normal [ID-token exchange](#apps-using-token-exchange-directly) or [authorization code grant](#apps-using-the-authorization-code-grant) requests.
2. **Migrate existing installations (for inactive users)**: Use the [direct migration flow](#migrate-existing-tokens-without-a-user-session) if you don't want to wait for users to interact with your app and trigger a new token acquisition.

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, then store the refresh token that comes back and use that one next time. 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.

***

## Migrate existing tokens without a user session

Use this migration flow if you don't want to wait for users to open your app and trigger a new token acquisition. It also covers background-only apps, such as webhook consumers, app proxy backends, and Flow actions.

This migration request uses the stored non-expiring token and your app's client credentials. It doesn't require an ID token or a user session.

**Caution:**

Migration permanently invalidates the old token for GraphQL Admin API requests. Store the access token and refresh token returned by the exchange.

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

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"}`.

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

***

## Handle errors during the transition

Existing installations migrate when your app acquires and stores an expiring token pair, through normal token acquisition or the [direct migration flow](#migrate-existing-tokens-without-a-user-session).

When Shopify rejects a GraphQL 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 `403` and 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.
* **Refresh token errors**: if a refresh token is expired, already replaced by one your app has since used, 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 replacing stored tokens.

***
