Skip to main content

About offline access tokens

When you create an API access token for the GraphQL Admin API, you can choose between two access modes: offline and online. 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.


Anchor to Expiring vs non-expiring offline tokensExpiring 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.

Anchor to Expiring offline tokensExpiring 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.

Anchor to Non-expiring offline tokensNon-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. This is a one-time, irreversible migration per shop.


Anchor to Acquiring expiring offline tokensAcquiring expiring offline tokens

Expiring offline tokens are supported in the different token acquisition flows.

Anchor to Token exchange from session tokenToken exchange from session token

When using token exchange, include the expiring=1 parameter:

POST https://{shop}.myshopify.com/admin/oauth/access_token
Parameters for the token exchange API and their descriptions
ParameterDescription
client_idrequiredThe API key for the app.
client_secretrequiredThe client secret for the app.
grant_typerequiredThe value urn:ietf:params:oauth:grant-type:token-exchange indicates that token exchange is to be performed.
subject_tokenrequiredAn ID token that represents the identity and active browser session of a merchant using the app.
subject_token_typerequiredThe value urn:ietf:params:oauth:token-type:id_token indicates that the subject token type is an ID token.
requested_token_typeThe 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.

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

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

{
"access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in": 3600,
"refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token_expires_in": 7776000,
"scope": "write_products,read_orders"
}

Anchor to Authorization code grantAuthorization code grant

When exchanging an authorization code for an offline access token, include the expiring=1 parameter:

POST https://{shop}.myshopify.com/admin/oauth/access_token
Parameters for the token exchange API and their descriptions
ParameterDescription
client_idrequiredThe client ID for the app, as configured in the Dev Dashboard.
client_secretrequiredThe client secret for the app, as configured in the Dev Dashboard.
coderequiredThe authorization code provided in the redirect.
expiringOnly 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.

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

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

{
"access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in": 3600,
"refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token_expires_in": 7776000,
"scope": "write_products,read_orders"
}

Anchor to Refreshing expiring offline tokensRefreshing expiring offline tokens

When an expiring offline token expires, use the refresh token to obtain a new access token and refresh token.

POST https://{shop}.myshopify.com/admin/oauth/access_token
Parameters for refreshing an access token
ParameterDescription
client_idrequiredThe API key for the app.
client_secretrequiredThe client secret for the app.
grant_typerequiredThe value refresh_token indicates that a refresh token grant is being used.
refresh_tokenrequiredThe refresh token received when the access token was issued.

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

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

{
"access_token": "shpat_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
"expires_in": 3600,
"refresh_token": "shprt_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"refresh_token_expires_in": 7776000,
"scope": "write_products,read_orders"
}

Anchor to Refresh token behaviorRefresh 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.


Anchor to Migrating from non-expiring to expiring tokensMigrating from non-expiring to expiring tokens

If your app currently uses non-expiring offline tokens, you can migrate to expiring tokens to enhance security.

Anchor to Step 1: Update your app's session storageStep 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

Anchor to Step 2: Implement token refresh logicStep 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.

Anchor to Step 3: Start requesting expiring offline tokensStep 3: Start requesting expiring offline tokens

For new installs, start acquiring expiring offline tokens, and persist the refresh token for refreshing.

Anchor to Step 4: Migrate existing tokensStep 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.

POST https://{shop}.myshopify.com/admin/oauth/access_token
Parameters for migrating to expiring tokens
ParameterDescription
client_idrequiredThe API key for the app.
client_secretrequiredThe client secret for the app.
grant_typerequiredThe value urn:ietf:params:oauth:grant-type:token-exchange indicates that token exchange is to be performed.
subject_tokenrequiredThe non-expiring offline access token to migrate.
subject_token_typerequiredThe value urn:shopify:params:oauth:token-type:offline-access-token indicates that the subject token is an offline access token.
requested_token_typerequiredThe value urn:shopify:params:oauth:token-type:offline-access-token for requesting an offline access token.
expiringrequiredMust be set to 1 to request an expiring offline token.

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

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

{
"access_token": "shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in": 3600,
"refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token_expires_in": 7776000,
"scope": "write_products,read_orders"
}

Was this page helpful?