Confused about subject_token for token exchange vs refresh (expired offline access token)

I’m implementing the new expiring offline access token flow and I’m confused about how to correctly use subject_token.

I originally obtained an expiring offline access token and a refresh token. Later, when that offline access token expired, I tried to call the token exchange endpoint using the expired access token as subject_token, and got this error:

Token exchange cannot be performed due to an invalid subject token.

From the docs, I understand:

  • For initial token exchange, subject_token should be the session token (ID token).
  • For migration from a non-expiring offline token to an expiring one, subject_token should be the non-expiring offline access token.
  • For refreshing an expiring offline token using a refresh token, I should not use subject_token at all, but instead use a refresh grant with refresh_token.

My questions:

  1. Once my expiring offline access token has expired, can I ever use it as subject_token in token exchange? (I assume no, based on the error.)
  2. If the merchant logged in a month ago, how can I get a current session token (ID token) to use as subject_token?
    • Is it correct that I cannot retrieve a valid session token from my database and must instead wait until the merchant opens the embedded app again so Shopify issues a new session token?
  3. In practice, for long-running/background jobs, is the correct approach:
    • Store the offline access token + refresh token, and
    • Use the refresh token flow (not token exchange) to keep the offline access token valid,
    • And only fall back to token exchange with a new session token when the refresh token has expired?

I want to confirm that I should never rely on old session tokens from my DB and that the only long-lived credentials I should persist are the offline access token and refresh token.

Reference: About offline access tokens

Q1. Once my expiring offline access token has expired, can I ever use it as subject_token in token exchange?
No. An expired offline access token cannot be used as subject_token. The subject_token must be valid.

Q2. If the merchant logged in a month ago, how can I get a current session token (ID token) to use as subject_token?
You cannot get a new session token from your database. A fresh session token is issued only when the merchant opens the embedded app.

Q3. Is it correct that I cannot retrieve a valid session token from my database and must instead wait until the merchant opens the embedded app again?
Yes. Session tokens are short-lived and should not be stored for reuse. Wait for the merchant to open the app to receive a new one.

Q4. For long-running/background jobs, is the correct approach to store the offline access token + refresh token, use the refresh flow, and only use token exchange when needed?
Yes.

  • Store the offline access token and refresh token.
  • Use the refresh token flow to obtain a new offline access token when it expires.
  • Use token exchange only to obtain a new offline token from a fresh session token (or during migration), not for routine token refreshes.

Thanks

Thank you, @Priyanka_Rawat for the response. Could you please share any hint for the below issue?

I want to add some more context here about the problem. I have used the below curl to retrieve the access token but it is returning 401status code with response, I am not sure why it is happening.

curl -X POST "https://SHOP_NAME.myshopify.com/admin/oauth/access_token" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id":     "YOUR_API_KEY",
    "client_secret": "YOUR_API_SECRET",
    "refresh_token": "YOUR_REFRESH_TOKEN",
    "grant_type":    "refresh_token"
  }'
{"error":"invalid_request","error_description":"This request requires an active refresh_token"}

Note: The refresh_token validity is 2026-09-29but still I can’t generate new access token.

With the below cURL, I stuck in the subject_token.

curl -X POST \
  https://SHOP_NAME.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'
{"error":"invalid_subject_token","error_description":"Token exchange cannot be performed due to an invalid subject token."}

This usually means Shopify does not consider the refresh token active. Even if you believe it is valid until 2026-09-29, verify that:

  • You’re using the latest refresh token (Shopify may rotate refresh tokens),
  • The token belongs to the same app and shop,
  • You’re using the correct client_id and client_secret,
  • The app has been migrated to the expiring offline token flow successfully.

Hi @Priyanka_Rawat, Could you please share me the flow on how can I execute background tasks after token exchange from non expired offline token to refresh token concept? What I have done yet-

  1. I have migrated from offline access token to refresh token using the below cURL for all my users.
  2. Now I am not able to create a new access token with the refresh token. So, I am not able to execute the background task for my user.

cURL #1

curl -X POST "https://{SHOP}/admin/oauth/access_token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Accept: application/json" \
  -d "client_id={API_KEY}" \
  -d "client_secret={API_SECRET}" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token={ACCESS_TOKEN_FROM_DB}" \
  -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"

Ref: About offline access tokens

cURL #2

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={TRIED_WITH_BOTH_ACCESS_TOKEN_AND_REFRESH_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'

Tried with both access token and refresh token in the cURL #2, it just returned-

{"error":"invalid_subject_token","error_description":"Token exchange cannot be performed due to an invalid subject token."}

Have I missed anything or doing anything wrong here?

Ref: Exchange a session token for an access token

Answer to your question, @Priyanka_Rawat

  1. I am using the latest refresh token
  2. Used same app and shop in the cURL
  3. Used the same client id, secret
  4. Couldn’t get your point- “migrated to the expiring offline token flow successfully”, I have migrated to that flow except the renewal of refresh token because I got the same error here- 520 during refresh_token rotation permanently orphans the offline access token

Could you please share where can I get subject_token to generate new access token share here- Exchange a session token for an access token when I have migrated to refresh token from here- About offline access tokens

Is there anything I missed?