520 during refresh_token rotation permanently orphans the offline access token

Setup: App using expiring offline access tokens, refreshing server-side near expiry with grant_type=refresh_token against POST /admin/oauth/access_token.

Issue: for a brief window this endpoint returned HTTP 520 (“error code: 520”) for a shop:

[2026-06-15 10:54:07 UTC] POST https://{shop}.myshopify.com/admin/oauth/access_token  ->  520 <none>
[2026-06-15 10:54:25 UTC] POST https://{shop}.myshopify.com/admin/oauth/access_token  ->  520 <none>

After that, the shop’s offline token was permanently orphaned — every subsequent refresh returned:

POST https://{shop}.myshopify.com/admin/oauth/access_token  ->  401
{"error":"invalid_request","error_description":"This request requires an active refresh_token"}

Refresh tokens are single-use / rotating, so it looks like the 520 hit after the old token was consumed/rotated server-side but before the new token was returned — leaving the token gone with no way to recover via refresh_token. The merchant was locked out for ~2 hours until we re-minted via token exchange.

Flagging the 520s on the token endpoint so the team can take a look — a server error there shouldn’t happen, and during refresh rotation it can permanently lock a merchant out. Happy to share exact timestamps / shop / request IDs.

Sounds like potentially a timing issue, where the expiration date on the access token isn’t exact, perhaps a bit earlier than what’s reported?

How close was the access token to expiring? Seconds, minutes?

Exactly, and I completely agree.

This is actually my biggest concern with expiring token migrations. At the beginning of June, I was about to migrate users on one of my apps when Shopify experienced a major API outage. The same thing happened again this morning: I was preparing another migration and started seeing intermittent API failures with responses such as “Internal error. Looks like something went wrong on our end” with a request ID to forward to the Shopify support.

If these failures occur during the refresh token rotation process, the consequences can be severe. My concern is that a refresh token could be consumed server-side while the response never reaches the app due to a timeout, 520, or internal error. In that scenario, the merchant could end up permanently locked out and forced to reinstall the app to obtain a new token.

For large-scale migrations, that’s a pretty scary failure mode. We really need a mechanism that guarantees token refreshes are atomic and recoverable, even when Shopify or the network experiences transient failures. A server-side error during token rotation should never leave an app without a valid way to authenticate.

Not seconds or minutes, it was already long expired. About 6 hours 50 minutes past expiry when the refresh that hit the 520 fired.

The timeline from our records:

  • Access token issued 2026-06-15 03:03:42 UTC, ~1h lifetime → expired 04:03:42 UTC.
  • The shop then sat idle (no API calls), so no refresh happened during the normal near-expiry window.
  • The refresh fired lazily on the merchant’s first request after reopening the app the two 520s were at 10:54:07 and 10:54:25 UTC, i.e. ~6h50m after the access token had already expired.

So this wasn’t a near-boundary race (token expiring mid-flight). The access token was stale-and-unused, and the very first refresh attempt to revive it is the one that caught the transient 520 which then consumed/rotated the refresh token without returning the replacement.

we have had a similar issue, access token expired about a week ago, refresh token still 3 months til expiry, unfortunately wasn’t able to get the error message, but we do retry if any issues and have ended up with stale access and refresh tokens.

I am experiencing the same thing. In my case, I have tried to test regenerating the refresh token, but it returned the same response {"error":"invalid_request","error_description":"This request requires an active refresh_token"}with 401 status code where the refresh token is just generated.

Hope to find a solid solution. Please correct me if I am doing anything wrong here to regenerate.

Hi folks,

Appreciate all the feedback here - we’re working on a solution for this. Will update here when there’s more to share.

@Liam-Shopify We run a public payments app. The expiring offline token changelog is tagged Admin API only and doesn’t appear under the payments-apps-api filter, but our API health warning is triggered by Payments Apps API calls (paymentSessionResolve) made with a non-expiring offline token. Support told us it applies here too, since it’s the same X-Shopify-Access-Token.

Can someone confirm:

  1. Does the requirement apply to Payments Apps API calls, or only Admin API?
  2. After Jan 1, 2027, will non-expiring offline tokens stop working for paymentSessionResolve?
  3. paymentSessionResolve is synchronous in checkout (its response returns the confirmation redirectUrl). For a low-volume shop whose 90-day refresh token expires during a no-order period, what’s the recommended way to avoid the next customer’s checkout failing with no merchant present to re-auth?

Hi folks,

Update on this issue: We’ve shipped a change today to make token refresh more resilient to transient failures during rotation.

Hi Liam,

can you provide details as to what those changes are and the parameters around the retry mechanism?

Thanks

Min

Amazing Liam is there any action we need to take regarding this?

Is there any architectural change here @Liam-Shopify ?

A bit more detail on the expected behavior and how apps should handle ambiguous refresh failures:

If a refresh request times out, encounters a network error, or returns a transient 5xx response, you can retry the request using the original refresh token. During the retry window, if that token was already rotated, the retry returns the same newly issued credentials rather than rotating them again.

Note: You should still treat the previous refresh token as spent once the new credentials are successfully received. The retry window is intended to accommodate normal retry and backoff behavior, rather than provide a fixed period that apps should build around.