Migrating from non expiring to expiring access tokens

From 2027 Jan 1 all apps will have to use expiring offline access tokens.

While this is a good change for security - It introduces real complexity for apps that heavily rely on the API. We handle over 50 thousand merchants and for most of them we run daily background tasks like image, content and data optimization.

My questions are:

  1. Could increase the expiring token from 1h to 24h to avoid refreshing it constantly for API heavy apps ?
  2. How do we migrate our customers to expiring tokens which we cannot reach over email and have not logged-in to our app for months, but rely on our app to optimize their store ?
  3. What happens if token exchange fails when using a refresh token due to Shopify or our app issues (DB timeout, worker crash, network blip on your side) ? Maybe we can get a retry count or grace period ? Like 3 retries due to failure or 5 minutes to retry.
  4. How do we handle seasonal stores re-open once or twice a year and then freeze until next event ?
  5. With the Jan 1, 2027 requirement for all public apps, is enforcement a hard cutoff or is there a phased rollout / grace period where non-expiring tokens return warnings before they hard-fail?
  6. What is the exact concurrency behavior of the one-time-use refresh token? If two requests submit the same refresh token within a few hundred milliseconds, is it hard-invalidated on first use, or is there a short grace window where the old refresh token still works?
  7. Can a single non-expiring token be exchanged for an expiring one more than once if our first exchange succeeds at Shopify but fails to persist on our side, or is that shop then locked out and requiring merchant re-auth?

Regarding 2.: From my understanding, its possible to exchange legacy access tokens for new refresh tokens without merchant interaction. See Step 4 About offline access tokens

Ah yes, I was reading about It too. While 2nd question is clear, the 7th one is really important when doing the migration. I’m sure a lot of people will mess this up and would be awesome to be able to retry a few times :sweat_smile:

Correct - you can exchange your existing offline access tokens for expiring access tokens without needing a user to open the app.

Hey @TerenceShopify @eytan-shopify

Can we get answers for other questions?

Also I understand that for expiring offline access tokens, Shopify allows only one active token per app/shop combination, and issuing a new token revokes the previous one immediately.

Would Shopify consider supporting a short grace overlap window, where the current token and immediately previous token are both valid for a limited time, or allowing a maximum of two active tokens temporarily?

Our app runs distributed background workloads and heavy async tasks, not only simple request/response merchant web pages. Immediate revocation during token rotation can create race conditions across workers, queues, and long-running jobs.

A short overlap window, for example 30–180 seconds, would make rotation safer while still keeping token exposure limited. Is there any recommended Shopify-side or app-side pattern for handling this in distributed systems?

When refreshing an access token the old access token remains active until its TTL has been reached. Refreshing doesn’t immediately revoke the old access token.

@elilip thank you for making this thread, you’re raising exactly the same concerns I have.

I’ve been playing around with migrating all of our tokens and it seems like the current refresh token system will cause significant harm to our customers in the event of any issue occurring (on Shopify’s side or ours) when getting a new token.

If a new token is generated and you are unable to store the refresh token for any reason, that store is essentially disabled in your app until they re-authenticate.

I am hoping Shopify can find a secure way to allow a refresh token to be used multiple times before being revoked, to allow apps to be secure without hindering their user experience.

Yes, agree here. Would be nice to have the client_credentials grand flow as fallback.

The planned change is not workable.

If the refresh fails at any point, the merchant will eventually have to re-install the app.

Who thought this was a good idea?

Maybe an update could be better here.

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.

Hi @TerenceShopify - glad to hear there are steps to securing merchant’s tokens.

Rotating the offline access token with a refresh token makes sense, it’s passive and it doesn’t impede the merchants workflow.

One quick question, from what I’m reading the refresh token expires after 90 days.

It appears the only way to generate a refresh token is through an session token.

Therefore, a merchant must re-open your app within 90 days for your app to continue to function on their behalf?

UPDATE: I think I answered my own question.

The access token and the refresh token are refreshed with the grant_type=refresh_token.

This way, as long as you’re refreshing tokens before the expiry, then the merchant doesn’t need to login to authorize another minting of a refresh token/access token pair from their session token.

The access token and the refresh token are refreshed with the grant_type=refresh_token.

Correct - each time you refresh the access token you will also receive a new refresh token with a new 90 day TTL for that refresh token.

@TerenceShopify When can we expect the answers to our other questions?

Do you have an AMA planned for this?

Sharing an undocumented gotcha that bit us hard — posting here for anyone else hitting the same wall while implementing the migration.

Shopify’s Token Exchange endpoint requires an undocumented expiring=1 field in the request body to return the modern expiring-token shape. Without it, the response is the legacy non-expiring shape, and the new Admin API rejects with:

[API] Non-expiring access tokens are no longer accepted for the Admin API. Start using expiring offline tokens.

This happens even when your active app version has use_legacy_install_flow=false and embed_app=true. The Partner Dashboard config alone is not enough — the request itself has to ask for the new shape.

Before (the wall)

Request body:

{
  "client_id": "...",
  "client_secret": "...",
  "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
  "subject_token": "<App Bridge session JWT>",
  "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
  "requested_token_type": "urn:shopify:params:oauth:token-type:offline-access-token"
}

Response (HTTP 200, but useless):

{
  "access_token": "shpat_...",
  "scope": "read_orders,..."
}

No expires_in, no refresh_token — and any subsequent Admin API call 403s with the deprecation error.

After (with expiring=1)

Add one field:

{
  ...same as above...,
  "expiring": 1
}

Response:

{
  "access_token": "shpat_...",
  "scope": "...",
  "expires_in": 3599,
  "refresh_token": "shprt_...",
  "refresh_token_expires_in": 7775999
}

That access_token then works against shop.json, graphql.json, GraphQL webhookSubscriptionCreate, everything.

Where I learned this

Confirmed via Shopify Partner Support after a full diagnostic walkthrough — advisor Salman pointed it out in about 4 minutes after I’d spent the better part of a day reading docs and ruling out app-config issues. He said he’d flag the docs team. Right now this field isn’t documented at:

If you’re migrating an app and your Token Exchange response is missing expires_in/refresh_tokenexpiring=1 is the answer.

Refresh-flow gotcha (also from Salman)

Standard OAuth refresh against the same endpoint:

{
  "client_id": "...",
  "client_secret": "...",
  "grant_type": "refresh_token",
  "refresh_token": "<stored refresh_token>"
}

Critical: the response returns a NEW refresh_token that invalidates the one you just sent. Store it atomically (same transaction as the new access_token) BEFORE any other caller relies on the access_token, or you lose the ability to refresh and the merchant has to re-launch the app.

After ~90 days (refresh_token_expires_in), the refresh token itself expires — only fix is the merchant re-launching the embedded app, which triggers a fresh Token Exchange and starts a new cycle.

Hope this saves someone the same diagnostic loop. @Shopify — would be great to have expiring=1 (or whatever the eventual canonical opt-in flag becomes) documented on the Token Exchange page; right now apps doing the right config-level migration silently get the wrong token shape from the API.

Hi @Eduard_Cristea

This behaviour is documented on

at Step 2:


But this could be a bit clearer for sure - will pass this feedback on to the team.

After implementing the migration, how can I test whether it will work correctly in the existing apps? For my existing app access tokens, which will start expiring in 2027, how can I test this before then?

cc: @Liam-Shopify

If you have it implemented you can test two ways. First, switch to at least 2026-01. There it will be deprecated and you should receive warnings. Next, switch to 2026-04. In that version they will not work and you’ll get errors.

You should have a token exchange that will swap the non-expiring tokens out with the expiring versions every time one is encountered. If you do then you should be good to go.

@Liam-Shopify
The flow in its current form is an infrastructure nightmare.

In real-world production environments, there are hundreds of points of failure. What if the network fails? What if the server crashes? What if the database write fails?

As soon as I refresh an access token and receive a new refresh token, the old refresh token becomes invalid.

Suppose I successfully make the token refresh request and receive the new tokens, but then the database write fails. Does the merchant now need to reopen the app and reauthorize it?

Imagine a merchant’s store is functioning perfectly fine, but something breaks on my/shopify infrastructure side, and I have to tell them to reopen the app to grant access again.

What I think is needed is something like this:

Assume we have:

RT1, AT1

I refresh the token and receive:

RT2, AT2

Shopify should invalidate RT1 only after I have made at least one successful Admin API call using AT2.

This would allow me to implement a safer workflow:

Refresh the token and receive RT2 and AT2.
Persist RT2 and AT2 to the database.
Only after the database write succeeds, make an Admin API call using AT2.
Shopify can then invalidate RT1.

This would significantly reduce the risk of losing access due to transient infrastructure failures such as database outages, network issues, or server crashes.

so, we run a carrier service app and serve about 1m requests a day.

We’re going to need to refresh the access token every 45min or so for each store installed in order to keep the access token always valid and to keep requests flowing quickly without impacting the already restrictive timeouts associated with the carrier service API.

I assume there will not be any throttling on the auth endpoints?

But, if the endpoint does go down for any reason, or the refresh fails midway (i.e invalidates the refresh token but fails to return it to us), this will mean merchants will immediately lose access to features and result in incorrect shipping rates until they log into the app again.

Is there going to be a more robust recovery method?

Hi,

We are currently looking into implementing this. As others have mentioned, though, the current approach may introduce some operational challenges, especially for apps running at larger scale. Our main concern is that it could unintentionally make apps on Shopify less resilient in certain edge cases.

We believe this could be improved significantly with two adjustments:
1.) The old refresh token shouldn’t become invalid until at least one API request was made with the new token. This resolves the issue where just after we request a new token, a database write fails (e.g. server crashes, db crashes, db is restarted, etc.) which results in refresh token becoming invalid. This requires the merchant to reinstall the app for it to work.
Which can be frustrating from the merchant’s point of view, as this kind of thing can stop working in the middle of the night and before they notice it isn’t working, days can go by.

2.) The access token expiration period could be extended.
Refreshing the access token every hour is a very short time span. While I understand that short time span increases security, but imagine running an API heavy app for 50k+ merchants. This essentially means that we have to refresh all 50k+ tokens once every hour. This means that the majority of API calls will consist of token refresh calls.
This can easily be reduced simply by increasing the expiration time of access token to 60 or 90 days.

It would be great if Shopify could consider making these adjustments, as this will make the whole platform feel more stable.