--- title: About offline access tokens description: Learn about offline access tokens. source_url: html: >- https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens md: >- https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md --- ExpandOn this page * [Expiring vs non-expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#expiring-vs-non-expiring-offline-tokens) * [Acquiring expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#acquiring-expiring-offline-tokens) * [Refreshing expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#refreshing-expiring-offline-tokens) * [Migrating from non-expiring to expiring tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#migrating-from-non-expiring-to-expiring-tokens) # About offline access tokens When you create an API access token for the [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql), you can choose between two access modes: offline and [online](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/online-access-tokens). Offline is the default access mode when none is specified. Tokens with offline access mode are meant for service-to-service requests where no user interaction is involved. Offline access mode is ideal for background work in response to webhooks, or for maintenance work in backgrounded jobs. *** ## Expiring vs non-expiring offline tokens As of December 2025, Shopify supports **expiring offline access tokens**, providing enhanced security through token rotation with a refresh token while maintaining the ability for apps to perform background operations without user interaction. ### Expiring offline tokens Introduced in December 2025, expiring offline tokens provide enhanced security by allowing apps to regularly rotate access tokens using refresh tokens. Apps can continue performing background operations without user interaction while maintaining better security through token expiration and app-managed token refresh. Expiring offline tokens have the following characteristics: * **90-day refresh token lifetime**: A refresh token is provided in the response when obtaining new access tokens. * **Token refresh**: Apps can refresh expired tokens without merchant intervention. * **Only one expiring offline token can be active per app/shop combination**: Acquiring a new expiring token will revoke all previous expiring tokens for that shop. ### Non-expiring offline tokens Prior to December 2025, non-expiring offline tokens were the default and only option for offline access. These tokens grant permanent access to a shop's data and can only be revoked through app uninstallation or secret revocation, making them less secure than expiring tokens. Non-expiring offline tokens have the following characteristics: * **No expiration**: Tokens remain valid indefinitely until app is uninstalled or secret revocation. * **Acquiring offline tokens**: Getting offline tokens for the same shop and installation returns the same access token each time. Note Apps can migrate from non-expiring to expiring tokens using the [token exchange grant](#migrating-from-non-expiring-to-expiring-tokens). This is a one-time, irreversible migration per shop. *** ## Acquiring expiring offline tokens Expiring offline tokens are supported in the different [token acquisition flows](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens). ### Token exchange from session token When using token exchange, include the `expiring=1` parameter: ```http POST https://{shop}.myshopify.com/admin/oauth/access_token ``` | Parameter | Description | | - | - | | `client_id`required | The API key for the app. | | `client_secret`required | The client secret for the app. | | `grant_type`required | The value `urn:ietf:params:oauth:grant-type:token-exchange` indicates that token exchange is to be performed. | | `subject_token`required | An ID token that represents the identity and active browser session of a merchant using the app. | | `subject_token_type`required | The value `urn:ietf:params:oauth:token-type:id_token` indicates that the subject token type is an ID token. | | `requested_token_type` | The value `urn:shopify:params:oauth:token-type:offline-access-token` for requesting offline access tokens. | | `expiring` | * `0` (default) for requesting a non-expiring offline token * `1` for requesting an expiring offline token | For more details on token exchange, refer to [Exchange a session token for an access token](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/token-exchange). #### Example The following example shows how to exchange a session token for an expiring offline access token. The response returns an access token, a refresh token, and their respective expiration times. ## Request ```terminal curl -X POST \ https://{shop}.myshopify.com/admin/oauth/access_token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d 'client_id={client_id}' \ -d 'client_secret={client_secret}' \ -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ -d 'subject_token={session_token}' \ -d 'subject_token_type=urn:ietf:params:oauth:token-type:id_token' \ -d 'requested_token_type=urn:shopify:params:oauth:token-type:offline-access-token' \ -d 'expiring=1' ``` ## Response ```json { "access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "expires_in": 3600, "refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "refresh_token_expires_in": 7776000, "scope": "write_products,read_orders" } ``` ### Authorization code grant When exchanging an authorization code for an offline access token, include the `expiring=1` parameter: ```http POST https://{shop}.myshopify.com/admin/oauth/access_token ``` | Parameter | Description | | - | - | | `client_id`required | The client ID for the app, as configured in the Dev Dashboard. | | `client_secret`required | The client secret for the app, as configured in the Dev Dashboard. | | `code`required | The authorization code provided in the redirect. | | `expiring` | Only applicable if the initial `https://{shop}/admin/oauth/authorize` request was for an offline token- `0` (default) for requesting a non-expiring offline token - `1` for requesting an expiring offline token | For more details on the authorization code grant flow, refer to [Implement authorization code grant manually](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant). #### Example The following example demonstrates exchanging an authorization code for an expiring offline access token. The response returns an access token, a refresh token, and their respective expiration times. ## Request ```terminal curl -X POST \ https://{shop}.myshopify.com/admin/oauth/access_token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d 'client_id={client_id}' \ -d 'client_secret={client_secret}' \ -d 'code={authorization_code}' \ -d 'expiring=1' ``` ## Response ```json { "access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "expires_in": 3600, "refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "refresh_token_expires_in": 7776000, "scope": "write_products,read_orders" } ``` *** ## Refreshing expiring offline tokens When an expiring offline token expires, use the refresh token to obtain a new access token and refresh token. ```http POST https://{shop}.myshopify.com/admin/oauth/access_token ``` | Parameter | Description | | - | - | | `client_id`required | The API key for the app. | | `client_secret`required | The client secret for the app. | | `grant_type`required | The value `refresh_token` indicates that a refresh token grant is being used. | | `refresh_token`required | The refresh token received when the access token was issued. | #### Example The following example demonstrates using a refresh token to obtain new tokens. The response returns a new access token and a new refresh token, both with updated expiration times. ## Request ```terminal curl -X POST \ https://{shop}.myshopify.com/admin/oauth/access_token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d 'client_id={client_id}' \ -d 'client_secret={client_secret}' \ -d 'grant_type=refresh_token' \ -d 'refresh_token={refresh_token}' ``` ## Response ```json { "access_token": "shpat_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", "expires_in": 3600, "refresh_token": "shprt_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "refresh_token_expires_in": 7776000, "scope": "write_products,read_orders" } ``` ### Refresh token behavior When you refresh an expiring offline token, the following behavior applies: * **New tokens on each refresh**: Both access token and refresh token are regenerated. * **Extended expiration**: A new refresh token has a new 90-day expiration from the refresh time. * **One-time use**: The previous refresh token is invalidated after use. Caution If the refresh token expires (after 90 days), the merchant will need to re-launch the app for the app to re-trigger the token acquisition flow. *** ## Migrating from non-expiring to expiring tokens If your app currently uses non-expiring offline tokens, you can migrate to expiring tokens to enhance security. ### Step 1: Update your app's session storage Add fields to store token expiration metadata: * `expires_at`: When the access token expires * `refresh_token`: The refresh token value * `refresh_token_expires_at`: When the refresh token expires ### Step 2: Implement token refresh logic Before making API requests where no user interaction is involved, like a background job, check if the offline access token has expired and [refresh it if needed using the refresh token](#refreshing-expiring-offline-tokens). ### Step 3: Start requesting expiring offline tokens For new installs, start [acquiring expiring offline tokens](#acquiring-expiring-offline-tokens), and persist the refresh token for refreshing. ### Step 4: Migrate existing tokens For installed shops with existing non-expiring tokens, perform the migration using token exchange. The migration can be done via a background job or during the next app launch. Caution The original non-expiring token will be revoked upon successful exchange. This migration is irreversible. To obtain a new non-expiring offline access token, the app would have to re-trigger the token acquisition flow with merchant interaction. ```http POST https://{shop}.myshopify.com/admin/oauth/access_token ``` | Parameter | Description | | - | - | | `client_id`required | The API key for the app. | | `client_secret`required | The client secret for the app. | | `grant_type`required | The value `urn:ietf:params:oauth:grant-type:token-exchange` indicates that token exchange is to be performed. | | `subject_token`required | The non-expiring offline access token to migrate. | | `subject_token_type`required | The value `urn:shopify:params:oauth:token-type:offline-access-token` indicates that the subject token is an offline access token. | | `requested_token_type`required | The value `urn:shopify:params:oauth:token-type:offline-access-token` for requesting an offline access token. | | `expiring`required | Must be set to `1` to request an expiring offline token. | #### Example The following example shows how to exchange a non-expiring offline token for an expiring one. The response returns a new expiring access token and refresh token, and the original non-expiring token is revoked. ## Request ```terminal curl -X POST \ https://{shop}.myshopify.com/admin/oauth/access_token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d 'client_id={client_id}' \ -d 'client_secret={client_secret}' \ -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ -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' ``` ## Response ```json { "access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "expires_in": 3600, "refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "refresh_token_expires_in": 7776000, "scope": "write_products,read_orders" } ``` *** * [Expiring vs non-expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#expiring-vs-non-expiring-offline-tokens) * [Acquiring expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#acquiring-expiring-offline-tokens) * [Refreshing expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#refreshing-expiring-offline-tokens) * [Migrating from non-expiring to expiring tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens.md#migrating-from-non-expiring-to-expiring-tokens)