A subscription brand with 3,200 active subscribers signs off on a replatform to Shopify. The product catalogue moves cleanly, the redirects are mapped, the theme is signed off. Then the first renewal run lands and a third of the billing attempts fail. Nothing is wrong with the new store. The cards were never in the old store to begin with. They sat in a vault at the payment gateway, and the gateway only releases them under conditions nobody read before the cutover date was set.
This is the article on migrating subscribers when card tokens are the constraint: which payment methods can actually move, which ones cannot, what Shopify’s own migration tooling does and does not do, how to stage the cutover so a decline spike does not read as churn, and what the maths looks like in Australian dollars. It is written for subscription founders and the developers and agencies running the move. Our customer and order data migration guide covers the general case in one sentence on this problem. This piece is the long version.
The token problem in one paragraph
A saved card on a subscription is not a card number. It is a reference, issued by a payment gateway, that only that gateway’s vault can redeem. Your old subscription app holds a pointer to the reference. Shopify holds nothing yet. Moving the subscriber therefore means one of three things: Shopify is pointed at the same gateway reference (the token stays where it is), the gateway copies the underlying card data to a new vault (a PAN migration), or the customer types the card in again. The whole project is working out which of those three each subscriber falls into, and doing it before you pick a cutover date.
A Shopify community thread opened in June 2026 and still active in August is a useful reality check. The consensus from merchants and app vendors in it: card survival depends on the gateway, not on the subscription app, and tokens are gateway-specific, so a change of gateway generally means re-collecting payment details. Raw card numbers cannot be exported by any migration tool. That thread is community discussion, not documentation, so treat individual claims in it as leads to verify with your provider. The direction of travel matches the official docs below.
Step 1: sort every subscriber into a cohort before you plan anything
The first deliverable is not a migration plan. It is a spreadsheet with one row per active subscription and a cohort letter on each row. We build it from the old platform’s export plus the gateway’s customer list, and we do it in the first week of the project because it decides the timeline.
| Cohort | Who is in it | Route to Shopify | Customer action |
|---|---|---|---|
| A. Portable | Card on Stripe, Braintree or Authorize.Net, expiry safely after the first renewal | Shopify’s remote payment method import, same gateway | None |
| B. Expiring | Card that expires on or before the first renewal date | Same route, but the renewal fails regardless of the token | Update card before first billing |
| C. Wallet | Apple Pay or Google Pay vaulted at the gateway | Not migrated as a card on any documented path | Re-authorise on Shopify |
| D. PayPal | PayPal billing agreement (IDs begin with B-) | Separate PayPal billing agreement import, needs reference transactions enabled | None if approved, otherwise re-authorise |
| E. Re-authorise | Direct debit, a gateway Shopify cannot import from, anything unrecognised | No token route | Re-authorise on Shopify |
The cohort rules come straight from Shopify’s subscription migration documentation, which lists Stripe, Braintree, PayPal Express and Authorize.net as the supported legacy gateways, with caveats per gateway: Stripe imports credit card payment methods only, Braintree imports credit cards and Apple Pay but not PayPal or Google Pay, Authorize.net needs both the customer profile ID and the payment profile ID, and PayPal needs the reference transactions approval from PayPal before the billing agreement import will work. Rather than hold those rules in someone’s head, we encode them. Here is the script we run against the export.
#!/usr/bin/env python3
"""Sort legacy subscribers into migration cohorts BEFORE touching Shopify."""
import csv, sys, datetime as dt
from collections import Counter, defaultdict
CARD_GATEWAYS = {"stripe", "braintree", "authorizenet"} # remote-create gateways per Shopify guide
WALLETS = {"apple_pay", "google_pay"} # Stripe-vaulted wallets do not migrate as cards
def cohort(row):
tt, gw = row["token_type"], row["gateway"]
if tt == "card" and gw in CARD_GATEWAYS:
exp = dt.date.fromisoformat(row["card_exp"])
nb = dt.date.fromisoformat(row["next_bill"])
# a card that lapses before the first renewal will fail whatever the token does
return "B expiring" if exp <= nb else "A portable"
if tt in WALLETS:
return "C wallet"
if tt == "billing_agreement":
return "D paypal"
return "E re-auth" # direct debit, unsupported gateway, anything else
rows = list(csv.DictReader(open(sys.argv[1])))
n, mrr = Counter(), defaultdict(float)
for r in rows:
c = cohort(r)
n[c] += 1
mrr[c] += float(r["price_aud"])
total, total_mrr = len(rows), sum(mrr.values())
print(f"{'cohort':<12}{'subs':>6}{'share':>8}{'AUD/mo':>10}")
for c in sorted(n):
print(f"{c:<12}{n[c]:>6}{n[c]/total:>8.1%}{mrr[c]:>10,.0f}")
need = sum(mrr[c] for c in n if c != "A portable")
print(f"{'TOTAL':<12}{total:>6}{'':>8}{total_mrr:>10,.0f}")
print(f"needs customer action: {1 - n['A portable']/total:.1%} of subs, {need/total_mrr:.1%} of AUD/mo")
We ran it on a synthetic 400-subscriber fixture we generated for this article (random gateway and token mix, Australian-dollar prices between $39 and $119 AUD, a 16 November 2026 cutover). It is not client data, and the split is invented, but the output shape is exactly what a real run produces. Two thirds of the fixture moved with no customer involvement. A third needed someone to do something.

The last line of that output is the number the founder cares about. It is expressed in subscribers and in monthly revenue, because the two differ: in the fixture, 33.2 per cent of subscribers need action but 33.7 per cent of monthly dollars do, since the wallet and expiring cohorts skew slightly higher in price. If your high-value plans sit disproportionately on wallets or direct debit, the dollar number will be much worse than the head count.
Step 2: choose the route for each cohort
There are three technical routes. Which applies depends on whether you are changing gateway, and the answer is usually forced by the commercial side of the project, not chosen by the developer.

Route one: same gateway, point Shopify at the existing token
If your cards live in a Stripe, Braintree or Authorize.net vault and you are keeping that gateway connected to Shopify, you do not move any card data at all. Shopify’s customerPaymentMethodRemoteCreate mutation creates a payment method on a Shopify customer from the gateway’s own identifiers. For Stripe that is the Stripe customer ID and payment method ID, for Braintree the customer ID and payment method token, and for Authorize.net the customer profile ID and customer payment profile ID. It needs both the write_customers and write_customer_payment_methods access scopes.
Two behaviours matter. The operation is asynchronous, so the payment method that comes back may be incomplete and you poll it until Shopify has validated the remote data. And Shopify does not fall back to a customer’s default payment method for Stripe and Braintree: if the specific payment method ID or token you supplied is wrong, the contract has no method, full stop. That is why the import is driven from the cohort sheet row by row, not from “whatever the customer’s default is”.
mutation ImportStripeCard($customerId: ID!, $stripeCustomer: String!, $stripePm: String!) {
customerPaymentMethodRemoteCreate(
customerId: $customerId
remoteReference: {
stripePaymentMethod: { customerId: $stripeCustomer, paymentMethodId: $stripePm }
}
) {
customerPaymentMethod { id instrument { __typename } revokedAt }
userErrors { field message }
}
}
Shopify also states that Stripe customised charge flows, including specialised payment flags, are not supported in this path and can lead to more authorisation failures. If your legacy integration sent custom flags on renewal charges, find that out in week one, not on renewal day.
Route two: move the card data between vaults
If you are changing gateway, the token cannot follow the customer, so the card data itself has to move from one PCI-compliant vault to another. For a store moving into Shopify Payments, that is a PAN migration. Recharge’s migration documentation, written for exactly this case, is specific about the conditions: it requires Shopify Plus, full cardholder and billing address information for each payment method, and it recommends keeping the legacy processor connected so any method that fails to import can still process. Apple Pay, Google Pay, PayPal billing agreements, SEPA and direct debit, and expired cards do not transfer through it. The same document notes that simply changing your active processor inside Shopify does not move card data between systems.
If the destination is Stripe, the process is run for you by Stripe’s data migrations team. Their documentation sets out the sequence: you request a data export from the previous processor, submit an import request to Stripe, the previous processor encrypts the file with Stripe’s PGP key and uploads it over SFTP, Stripe sends a validation summary, you approve, and Stripe returns a mapping file from old IDs to new ones. Stripe quotes about 10 business days after it receives correct data, but is clear that the variable is the old processor, whose export can take anywhere from a few days to several months. CVC codes cannot be imported, and subscriptions are a separate Stripe billing migration, so they are not part of the card copy. Stripe adds one more instruction that applies to every route: cancel the old subscriptions as soon as the new ones are active, or customers are billed twice.
The practical consequence for timeline is that the PAN route is gated by a third party you do not control. If your old processor takes eight weeks to produce the file, your cutover date is eight weeks later than you thought, and you want that discovery on day three, not day sixty. Ask for the export in the first week and put the request date in the project plan.
Route three: ask the customer
Everyone in the wallet, direct debit and expiring cohorts, and everyone left over from a failed token or PAN import, ends up here. This is not a failure of the migration. It is a designed route with its own copy, its own timeline and its own measurement. Treating it as a cleanup task after launch is how brands lose subscribers they had already earned.
Step 3: build the contracts without charging anyone by accident
Once customers and payment methods exist in Shopify, the contracts themselves go in with subscriptionContractAtomicCreate, per Shopify’s contract migration guide. A contract carries the variant, selling plan, quantity, pricing policy, billing and delivery policy, addresses, and the payment method ID. The guide’s own example sets nextBillingDate explicitly on the contract, which is the field that stops a migrated contract from billing on the day you create it.
The part that protects you from double billing is the payment processing policy. By default, a billing attempt requires a valid payment method on the contract. If you set the policy to skip payment and create an unpaid order, Shopify creates an order that can be paid by invoice or manual capture. That is a useful tool for the re-authorisation cohort, because it keeps the subscriber’s shipment and order history intact while payment is sorted out, though you should decide deliberately whether you ship goods against an unpaid order, because that is a commercial risk, not a technical setting.
Billing attempts take a client-generated idempotency key, which Shopify describes as being there to avoid duplicate payments. Do not use a random UUID per call. Derive the key from the contract, the billing cycle and the attempt number, so a retry after a timeout reuses the same key and cannot charge twice. We cover the wider pattern in our piece on idempotent API writes. Here is the helper, and the output from running it.
import hashlib
def key(contract_id: str, cycle_date: str, attempt_no: int = 1) -> str:
"""Same contract + same cycle + same attempt number = same key, on every retry."""
raw = f"{contract_id}|{cycle_date}|{attempt_no}"
return "mig-" + hashlib.sha256(raw.encode()).hexdigest()[:24]
c = "gid://shopify/SubscriptionContract/501"
print("run 1 ", key(c, "2026-11-17"))
print("retry (same)", key(c, "2026-11-17"))
print("next cycle ", key(c, "2026-12-17"))
print("2nd attempt ", key(c, "2026-11-17", 2))
run 1 mig-a604d1423329841e44807f3b
retry (same) mig-a604d1423329841e44807f3b
next cycle mig-1e96f340798faf2b76df140f
2nd attempt mig-4eb58a28c68d1ff09ff8db97
The retry produces the identical key and therefore the identical outcome. A new cycle and a deliberate second attempt each get their own. A dunning attempt three days after a decline is attempt two. A network retry of attempt one is still attempt one.
For thousands of contracts, do not loop through single mutations. Shopify’s migration guides point to the Bulk Operations API for large volumes, which avoids the API limit costs of per-call mutations. We have written about the mechanics in our Bulk Operations API guide and the cost model behind it in API rate limits.
Step 4: pause the old app, then ramp, do not flip
Shopify’s customer migration documentation gives one instruction in plain terms: pause any billing in the current subscription application while migrating customer payment methods. The failure it prevents is a subscriber charged by the old app and by Shopify in the same cycle. That is the fastest route to chargebacks, refunds and a social media thread.
The other timing issue is what happens at the card network. Stripe’s migration documentation warns that your new account has its own merchant identifier, issuers may not recognise it at first, and you may see a temporary rise in declines coded generic_decline, do_not_honor, transaction_not_allowed or try_again_later. Their recommendation is to ramp volume rather than switch everything at once, and to send the highest-quality traffic first: customer-initiated before recurring, domestic before international. A community reply in the thread above adds a further risk that is not in the documentation we found: network transaction IDs from the original authorisation may not travel with a vault transfer, so renewals can fail even when the token is valid. Treat that as a question for your gateway, in writing, before cutover.
We turn all of that into a staged schedule instead of a single date.

| Week | Action | Exit test |
|---|---|---|
| T minus 3 | Written confirmation from old and new gateway on token and transaction ID carry-over. Notice email to cohorts B, C and E. | Both providers confirm in writing |
| T minus 2 | Reminder with one-tap update-payment link. Canary import of about 5 per cent of cohort A. | Canary contracts show valid payment methods |
| Cutover week | Pause old billing. Import contracts with next billing date set. Bill the canary cycle first. | Canary decline rate within an agreed band of the old baseline |
| T plus 1 | Ramp to roughly 25 per cent, then the rest. Compare migrated subscribers against native ones daily. | No cohort-A decline gap against native subscribers |
| T plus 4 | Second and third re-authorisation nudges. Close the old app subscriptions. | Old app shows zero active billing |
The exit tests matter more than the dates. The comparison between migrated and native subscribers in the first week is the single most useful control, and it is a suggestion from the community thread, and it is the control we recommend. If migrated cohort-A subscribers decline at a visibly higher rate than customers who subscribed natively on Shopify, the problem is the migration, not the customers, and you stop the ramp.
Step 5: design the re-authorisation journey like a funnel
Cohorts B, C and E need to do something, and the usual failure is copy that reads like a system notice. We treat it as a three-touch funnel with a single action: a link that opens the customer account page where they can add a payment method.
| Touch | Timing | Content | Single action |
|---|---|---|---|
| 1. Heads-up | 14 days before first Shopify billing | We are upgrading your account. Your next order is not affected if you confirm your card. | Confirm payment method |
| 2. Reminder | 5 days before | Order date, price in dollars, what ships. A plain link to update. | Confirm payment method |
| 3. Last call | 1 day before | Your next box is held for 48 hours. No charge until you confirm. | Confirm payment method |
Shopify’s contract guide points to the customer_payment_methods/create webhook as the signal that a new payment method has been added. We listen for it and flip the contract from awaiting payment to active the moment it fires, so the customer is never left wondering whether their click worked. The same webhook drives the suppression list for the reminder emails, so nobody who has already updated gets nagged.
Do not write these as marketing. The reader is a current customer who did nothing wrong, and the message is an operational one. Give them the date, the amount, the product and the link. Every line that is not one of those four things is friction.
What it costs when this goes wrong, in dollars
Australians spend heavily on subscriptions. ING reports roughly $26.5 billion a year in Australian subscriptions, with 78 per cent of adults paying for at least one service and an average of two each. That is a market where customers expect renewals to just work, and where a failed renewal reads as a broken brand rather than an expired card.
Here is the model we put in front of founders. It uses the cohort split from the fixture above and a plausible subscription business: 3,200 active subscribers at an average of $68 AUD a month, which is $217,600 AUD in monthly recurring revenue. If 33.7 per cent of that value sits in cohorts that need customer action, that is about $73,331 AUD a month that depends on customers acting. The next table varies one assumption only: the share of that group who never complete the re-authorisation. These are scenarios, not benchmarks. We do not have a trustworthy industry figure for it, and we would rather say so than invent one.
| Share who never re-authorise | Monthly revenue lost (AUD) | Annualised (AUD) |
|---|---|---|
| 15 per cent | $11,000 AUD | $131,996 AUD |
| 30 per cent | $21,999 AUD | $263,992 AUD |
| 45 per cent | $32,999 AUD | $395,988 AUD |
The spread between the first and last row is $264,000 AUD a year of recurring revenue, and the only thing that moves you between them is the quality of cohort sorting, notice, and follow-up. For context, the Recurly benchmark cited by Airwallex puts involuntary churn at 1.30 per cent for customers paying $10 to $25 a month, falling to 0.18 per cent above $250 a month, in global figures that are not Australian. Migration-day failure is a different animal to steady-state involuntary churn, but the comparison shows how much of a one-off event this is: a normal month loses a rounding error, a badly planned cutover loses a double-digit share of the base in one cycle.
The checklist we sign off before cutover
- Every active subscriber has a cohort letter, and the cohort totals reconcile to the old platform’s active count.
- Written confirmation from the old gateway about token or PAN export, and from the new gateway about import and network transaction ID handling.
- The export request date is in the project plan, because the old processor sets the timeline.
- The payment method ID, not the customer default, is mapped for each Stripe and Braintree row.
- A next billing date is set on every contract, and billing in the old app is paused.
- Billing attempts use derived idempotency keys, and the bulk run is tested on a canary cohort.
- The three-touch re-authorisation flow is built and the payment method webhook is wired to suppression and activation.
- A daily migrated-versus-native decline report exists before the ramp starts.
How we do it at Insiteful
Subscription migrations are one of the most consequential builds in our replatforming work, because they are the only part of a store move that touches money on a schedule. On every one we run the cohort sort in the first week, before design is finished, and we send the gateway export request the same day. That one habit protects the timeline more than any technical decision does. We write the cohort rules as code, so the numbers are repeatable and a founder can see their own split on a screen, not in a slide.
We also refuse to cut over on a calendar date. The cutover happens when the canary cohort passes its exit test, and the plan names who stops the ramp if migrated subscribers decline more than native ones. The general cutover mechanics, rollback and sign-off sit in our migration cutover runbook, and the ongoing subscription build is covered in subscription architecture. If you run a subscription business that has already committed to Shopify and has a vault of cards somewhere else, the cohort sheet is the first thing to ask for, and we will build it with you.
Sources and caveats
Everything in the official-process sections comes from Shopify’s developer documentation for migrating subscription customers and contracts and for the customerPaymentMethodRemoteCreate mutation, Stripe’s payment data import documentation, and Recharge’s help article on PAN migrations into Shopify Payments, all read in October 2026. Gateway rules change, so confirm them with your provider before you commit a date. The community thread referenced above is not authoritative. The 400-subscriber dataset and the cohort split are synthetic, generated for this article, and the revenue scenarios are assumptions we chose, labelled as such. ING provided the Australian subscription spending figures and Recurly, via Airwallex, the involuntary churn tiers.
Want us to size your migration before you commit a date?
Send us the export from your current subscription platform and we will return a cohort split, a route per cohort and a realistic timeline, in dollars. Start with our build assessment.