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:
- 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.)
- 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?
- 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-
- I have migrated from offline access token to refresh token using the below cURL for all my users.
- 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
- I am using the latest refresh token
- Used same app and shop in the cURL
- Used the same client id, secret
- 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?