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.