Please add recovery parity with refresh tokens

Shopify recently added more resilient recovery for lost refresh responses. We need the same for cycling a non-expiring offline token to an expiring pair. Losing that response permanently orphans the install: the old shpat_ is destroyed, retry returns invalid_subject_token, and the only fix is merchant re-auth.

Docs

What we believe happened

  1. Shop had a legacy non-expiring offline access token (shpat_…, length 38).
  2. No refresh token / expiry fields were populated (never successfully cycled).
  3. With expiring offline tokens enabled, our app ran the token-exchange cycle described in that section: subject_token = stored non-expiring offline token, expiring=1, no merchant session.
  4. Shopify appears to have accepted the exchange and destroyed the subject token, but our app did not persist the returned access token + refresh token (lost HTTP response, worker crash, or DB write failure, same failure class as the refresh case in your changelog). This was not a clean total outage: across the same app, the cycle succeeded for some merchants and orphaned others, with no obvious pattern. That points to an intermittent lost-response / failed-persist failure during cycle, not a systematic misconfiguration that would have broken every install.
  5. Live DB ended up with an empty offline access token; our pre-outage backup still had the original shpat_.

Evidence

Live row (before recovery attempt)

Field Value
Offline access token empty
Refresh token empty
Access / refresh expiry null
password_updated_at 2025-10-23

Backup row (same shop)

Field Value
Offline access token present (shpat_…, len 38)
Refresh token empty
Expiry columns null
password_updated_at 2025-10-23

Recovery attempt

  1. Restored the non-expiring offline token from backup onto live.
  2. Re-ran the cycle exchange from “Cycle existing tokens without waiting for a merchant”.

Shopify response

invalid_subject_token

No new access token / refresh token was returned that we could store.

This matches your own docs on that page:

Cycling is irreversible, and it isn’t safe to replay. Shopify destroys the non-expiring token in the same transaction that issues the expiring one, so if your app loses the response, that store has no usable token and the merchant has to reauthorize…

and:

When a parameter doesn’t meet these rules, Shopify returns 400 Bad Request with {"error": "invalid_subject_token"} … including for a store you’ve already cycled.

So the docs describe the failure, but there is still no recovery path other than merchant re-auth, unlike refresh, where you recently added a retry window.

Contrast: refresh recovery

This shop had no refresh token, so the new “previous refresh token stays valid until replacement is used” behaviour cannot apply. The orphan was created at cycle / migration exchange, not at refresh.

Why this is a gap

Your refresh resilience fix covers:

refresh succeeded at Shopify → app never saved the new pair → retry old refresh token

Cycling has the same failure shape, but worse outcomes:

cycle succeeded at Shopify → old shpat_ destroyed → app never saved the new pair → retry subject token → invalid_subject_token → permanent orphan until merchant opens the app

Your migration docs already warn this is unsafe to replay. I’m asking you to close that hole the same way you did for refresh, especially for background-only apps that can’t wait for a merchant to open the app (which that same section says is why cycling exists).

Ask

Please ship a real recovery path for a lost cycle response, not another doc warning.

Developers need one of:

  1. Parity with refresh recovery: keep the previous non-expiring offline access token valid until the app successfully begins using the replacement refresh token (bounded window), or
  2. Idempotent / retryable cycle: a second call with the same subject token after a lost response returns the same usable expiring pair instead of invalid_subject_token, or
  3. Partner-side recovery: a supported API for the app owner to re-issue offline access for an already-installed app without a merchant browser session.

Developers need a solution, not a warning

This failure mode should never have been left as “merchant must reopen the app.”

You designed a one-way cycle that destroys the old token in the same transaction that issues the new pair. Your own docs say losing the response orphans the store. That is not an edge case for hobby apps, it is a production outage for every public app that runs webhooks, jobs, Flow actions, or scheduled sync without a merchant sitting in admin. Those are exactly the apps your cycle section says need this API.

You already know lost responses happen: that is why refresh recovery exists. Extending resilience to refresh while leaving cycle as irreversible and non-retryable means apps that followed your migration guidance are the ones left with dead installs and no server-side way back.

A caution in the docs is not a solution. If Shopify knows this can permanently brick an install, Shopify needs to give developers a recovery grant, an idempotent retry, or a Partner recovery API, before more of the ecosystem is forced through this path ahead of January 1, 2027.

I’m happy to share Partner app ID, timestamps, and request IDs privately with Shopify staff.

Thanks for reporting this, @Jon_Purvis :folded_hands: I’ve still postponed the migration and am hoping Shopify will push the deadline to April 2027, until it’s actually safe to proceed.

I totally agree with your comment that developers need a solution, not a warning.

I wish they would simply keep the current old offline tokens valid until the deadline and, until then, make it possible to issue new tokens with that old offline token. It would make the whole process so much better and less stressful, knowing there’s a way back if something goes wrong.

Yes, thanks for pointing that out @Jon_Purvis. I’ve migrated 3.5 out of my 5 apps so far. Three of them are pretty simple admin apps with no webhooks and relatively low activity, and I haven’t had any issues so far.

I’m now migrating my 4th app, which relies heavily on webhooks. Hopefully everything goes smoothly, but I’d really like to monitor it for a few months before migrating my very last and most popular app.

So again, I completely agree with you @Philo. Hopefully Shopify will postpone the deadline to a later date. January 1st would probably be enough if BFCM and the Christmas period weren’t right in the middle of it!

Oh believe me, postponing is something I wish I did :joy: I guess seeing the deadline made me want to get ahead of the curve.

Shopify is full of smart people, so I don’t understand how they managed to mess something as fundamental as this up so much.

Given the radio silence in the other thread did actually produce a solution for that particular part of the process, I’m hoping the same happens here.

Is the .5 of an app one that’s currently getting migrated?

Just out of interest, what’s the average number of stores you’ve migrated per app?

The .5 is my 4th app currently being migrated. It is because I usually start by enabling expiring tokens for new installs only, then migrate existing stores in batches over a few weeks until everyone has been moved over :wink:

I’m currently at 5.5 apps migrated, with 1.5 apps still left to migrate.

And I fully agree with you guys. When I migrated my first app, I actually managed to miss storing the new token during the migration, and unfortunately, I didn’t have a backup of it either. :sweat_smile:

Luckily, that app doesn’t depend on background call, webhooks, or other server-side processes, so it wasn’t a complete disaster. Now, whenever the merchant comes back and opens the app again, re-authentication happens and the new token gets stored, so everything starts working again.

So I guess I got lucky with my first migration. But for apps that depend heavily on background processes or webhooks, losing that response is a very different story. That’s where having some kind of recovery path would definitely save a lot of pain.

So yes, I fully agree with the point here: a warning in the docs is helpful, but a recovery mechanism would be even better. :grinning_face_with_smiling_eyes:

Thanks for giving some input! I think the more people that can comment their experience here, the more chance we have of getting Shopify to do something about it.

For us, all of our apps ended up with merchants that worked and merchants that didn’t work, so the process is definitely flaky. There’s too much that needs to all work all at once, for this to actually work successfully.

The batch idea is a good one actually, hopefully your 4th app goes through the process smoothly!

Because they don’t care, lose a partner, another will replace it. As simple as it is…

This is completely valid feedback and thank you @Jon_Purvis for your detailed post and explanation. We will be addressing this to make sure the exchange is more resilient to these scenarios.

Thank you for validating this and committing to making the exchange more resilient going forward @TerenceShopify.

However, we need clarity on how Shopify plans to handle stores that are already in a broken authentication state right now.

A forward-facing fix prevents new failures, but it doesn’t help the merchants currently stranded in production. Specifically, we need a remediation path for two specific scenarios:

  1. Missing token: The stored token no longer exists in app databases.

  2. Revoked token: The token exists in app databases, but it has already been revoked or invalidated on Shopify’s end.

Currently, both scenarios leave developers with zero programmatic way to re-authenticate without forcing a manual uninstall and reinstall.

Is Shopify working on a backfill mechanism or recovery flow to restore authorisation for these already-affected stores?

That’s a pretty short-sighted way of looking at it. Sure, another partner might take their place, but an ecosystem built on “someone else will replace you” isn’t a healthy one.

Exactly, I don’t think Shopify is an healthy ecosystem right now and I wouldn’t recommend anyone starting a business related to it today. I do think that they don’t care about partners individually, as they don’t care about individual merchants, as long as the crowd is there. One more, one less, not a big deal to them, they’re an industry leader and the money comes in anyway.

Might be short sighted as you say, that’s my current feeling and the one of some others I discussed with.

@TerenceShopify

Please could we get an update on this?

Is Shopify working on a backfill mechanism or recovery flow to restore authorisation for already-affected stores?

Hi Jon - yes we’re actively working on both. We should have an update live soon that addresses the initial token exchange resiliency. We’re also working on providing better long term recovery paths for acquiring access tokens from stores when the app has lost its valid refresh tokens.

Hi @TerenceShopify Thank you for keeping us updated of what you’re currently working on. It’s appreciated.

FYI we’ve changed the token exchange process when exchanging non-expiring access tokens for expiring access tokens (with refresh tokens). TL;DR: this process can now be repeated for up to seven days, allowing for apps to perform the exchange again if the new expiring tokens fail to be stored during the first exchange.

This process is documented here.

For sake of your business & sanity I suggest to you to reconsider your position on this topic as soon as possible and adjust accordingly.

Shopify team have made no good decision for years. Dev experience gets worse and worse with no end in site. Did you see how they remove 1000s of valid app review? Not hard to see big issue like this on horizon. History is predictor of future. ATM I have close to 0 confidence in product team working on developer side of company. This forum topic is one of sooooo many problems they have created for partners.

Thank you @TerenceShopify ideally this would have been handled better from the start, but we (at least I) appreciate the follow up.

As for the deadline to migrate, do you have any update? We’d appreciate something like Q2 2027.

Thank you