Is the offline-token migration exchange (non-expiring → expiring, expiring=1) idempotent and safe to retry?

Hi Shopify team

Note: This is specifically about the migration token exchange (expiring=1) idempotency/retry, which differs from existing offline-vs-online token threads (e.g. rate-limit / bulk-operation visibility). I couldn’t find an existing answer for the migration-exchange retry case.

Context

We’re migrating our public app’s existing non-expiring offline access tokens to expiring ones ahead of the Jan 1, 2027 requirement. For already-installed shops we plan to run a one-time, irreversible migration exchange as a background batch across ALL shops (no merchant reinstall), using:

POST https://{shop}/admin/oauth/access_token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<existing NON-expiring offline token>
subject_token_type=urn:shopify:params:oauth:token-type:offline-access-token
requested_token_type=urn:shopify:params:oauth:token-type:offline-access-token
expiring=1

Already confirmed (so no need to repeat): the original non-expiring token is revoked on successful exchange, the migration is irreversible, and if we fail to persist the new token the shop must reinstall.

My open questions are specifically about retrying this migration exchange, which the docs cover for the refresh_token grant but NOT for the migration exchange.

Q-A: Idempotency / replay window

The refresh_token grant documents a replay window — “Shopify returns the same refreshed token response for up to 1 hour after the original refresh.”

Does the same replay window apply to the migration exchange above? i.e. if I resend the identical request with the same subject_token within some window after Shopify already processed it, do I get back the same access_token / refresh_token?

Q-B: Safe retry on transient failures

For refresh_token, the docs say to treat network errors / timeouts / 5xx / 429 as transient and retry with the same token.

Does the same retry-safety apply to the migration exchange? Concretely: if I send the migration exchange and the response is lost (timeout), so I don’t know whether Shopify processed it —

  • If I retry with the same subject_token, will Shopify return the same new token (recoverable)?
  • Or, since the old token may already be revoked, does the retry fail and leave the new token unrecoverable → shop orphaned (reinstall required)?

Why this matters

This runs once, irreversibly, across all existing shops. Whether a transient network blip during the batch can orphan a shop (vs. being harmlessly retryable) directly determines our failure-recovery design and the blast radius of the migration.

Is there any documented / guaranteed behavior here — a replay window, safe-retry guidance, or an idempotency-key mechanism — for the migration exchange specifically? Thanks!

Hey @masahiko_aruga - thanks for flagging this, and you’re right to separate the one-time migration exchange from the normal refresh flow.

I took a closer look on our end, and the migration exchange does not currently have the same one-hour replay behaviour as a refresh_token request.

I’m going to check internally for you here and I’ll loop back once I have confirmed guidance or a change to share. I definitely get that this needs a reliable answer before running an irreversible batch across every installed shop.

Thanks so much, @Alan_G — really appreciate you digging into this and separating the migration exchange from the normal refresh flow.

Good to know that the migration exchange doesn’t currently share the one-hour replay behaviour of a refresh_token request. Until you’re able to confirm the official guidance, we’ll design our batch conservatively on that basis — treating the migration exchange as non-idempotent and not safe to auto-retry on an ambiguous outcome:

  • persist a per-shop “pending” marker before sending the exchange,
  • on timeout / 5xx / no-response, flag the shop for manual review instead of auto-retrying,
  • and only mark a shop complete once the new token is atomically stored.

Our biggest concern is this: today, the only recovery we’re aware of when the exchange succeeds on Shopify’s side but we fail to persist the new token is asking the merchant to reinstall the app. Across every installed shop, forcing reinstalls on affected merchants would be a serious problem for us, so we’d very much like to avoid any merchant-facing reinstall.

So alongside the idempotency/replay question, could you please advise on:

  1. Is there any recovery path that does NOT require the merchant to reinstall if a shop ends up in the “old token revoked, new token not persisted” state? (e.g. a way to re-issue or re-exchange from the app/partner side, a grace period on the old token, or an idempotency key we can supply.)
  2. After a lost/failed response, what’s the recommended way to distinguish “exchange already succeeded on Shopify’s side” from “never processed” — ideally without any merchant action?
  3. Any replay window or safe-retry mechanism for the migration exchange that we can rely on.

No rush — I understand you’re checking internally. Avoiding merchant reinstalls is the key outcome we’re hoping for before running this irreversible batch. Thanks again for looping back, and I’ll watch this thread for your confirmed guidance. :folded_hands:

Whilst the latest changes (1 hour retry window) is a great improvement, i would also echo my concern for an alternative recovery method which does not involve merchant interaction should everything go pear shaped!

@Min_Liu
Thanks for chiming in — glad it’s not just us hoping for a merchant-free recovery path. :folded_hands:

Quick clarification on one point: when you mention “the latest changes (1 hour retry window),” do you mean the migration exchange now has a 1-hour replay window, or are you referring to the existing refresh_token flow? @Alan_G mentioned earlier that the migration exchange doesn’t currently share the refresh flow’s one-hour replay behaviour, so I want to make sure I understand correctly — if the migration exchange does now have a retry window, that would materially change our failure-handling design.

Either way, fully agree the key missing piece is a recovery path that doesn’t require the merchant to reinstall.

I’m talking about the refresh flow, we haven’t had any issues with the migration flow and bulk migrated all users. the refresh flow only recently had the 1 hr retry added, previously this was undefined and we found the API failed several times already.

Hey folks - thanks for clarifying, @Min_Liu. Glad to hear the bulk migration went smoothly on your end.

I’m still checking the migration-specific retry and recovery behaviour raised by @masahiko_aruga, since that lost-response edge case is still worth confirming. I’ll loop back once I have clearer guidance. In the meantime, if either of you runs into any migration errors, let me know and I’m happy to dig in further.

Hey @Min_Liu and @masahiko_aruga - looks like you folks may have seen this, but we’ve recently pushed some changes that should help resolve this:

Please let me know if you’re seeing any other issues though - happy to help :slight_smile:

Thanks @Alan_G — the refresh-flow resilience change (retrying a refresh with the same refresh_token and getting the same rotated credentials back) is great, and we’ll definitely lean on it for our steady-state refresh handling. :raising_hands:

Just to make sure we’re aligned: that change covers the refresh_token rotation flow, but my original question here is about the one-time migration exchangegrant_type=token-exchange with subject_token = an existing non-expiring offline token and expiring=1. As far as we can tell, that shipped resilience change doesn’t extend to the migration exchange (Darius confirmed the retry window applies to the refresh flow).

So for the migration exchange specifically, could you confirm:

  1. Is it idempotent / does it have any replay window (resend the same subject_token after a lost response → same new tokens)?
  2. Is it safe to retry with the same subject_token on transient failures (timeout / 5xx / no response)?
  3. If the old non-expiring token is revoked but we fail to persist the new token, is there any recovery path that doesn’t require the merchant to reinstall?

These matter because it’s a one-time, irreversible migration across every installed shop. Appreciate you looping back! :folded_hands:

Hey @masahiko_aruga - I think you’re right. The change I linked applies to refresh_token rotation and likely doesn’t resolve the one-time migration exchange case. I’m going to follow up specifically on the migration replay and recovery gap.

For now, there’s no documented replay guarantee I can point you to for that exchange, so your conservative handling is appropriate. I’ll loop back once I have confirmed guidance on retry behaviour and the available recovery paths.

Thanks for confirming, @Alan_G — really appreciate you separating the refresh_token rotation change from the one-time migration exchange, and for following up specifically on the migration replay and recovery gap. :folded_hands:

That matches our understanding. We’ll wait for your confirmed guidance before finalizing our failure-recovery design — especially on whether there’s any replay window / safe-retry for the migration exchange, and any recovery path that doesn’t require the merchant to reinstall.

No rush, and thanks again for chasing this down internally. I’ll keep an eye on this thread. :raising_hands: