I led the migration of Babbel's recurring billing from Adyen to Stripe: about a million subscriptions, more than $100M in recurring revenue, finished with zero customer-visible downtime. This guide is the writeup I wish had existed before we started. It covers what a subscription migration actually involves, where the real risks live, and the lessons that cost us weeks to learn.
It is written for engineering leaders and founders planning a move off Adyen (or any provider) onto Stripe Billing. Nothing here is theory. Everything below happened in production, at scale, with revenue on the line.
What "zero downtime" actually means
A payment provider migration has no maintenance window to hide in. Subscriptions renew every hour of every day, and the worst outcomes are symmetrical: bill someone twice and you create chargebacks and support tickets; miss a renewal and you create silent revenue loss that reconciliation may never catch.
The mechanism that makes zero downtime possible is the renewal boundary takeover. You never move a subscription mid-cycle. Instead, Stripe schedules each migrated subscription to begin exactly when its current cycle ends: the old provider bills the last renewal it owns, the new provider bills the next one, and the customer keeps their payment method, price, and billing day. There is no moment when both providers own the subscription, and no moment when neither does.
Everything else in this guide exists to protect that invariant at scale, for every subscription, including the weird ones.
The shape of a migration platform
For a subscription base of any real size, the migration is not a script. It is a small system that runs beside your production billing for months. Ours had five layers, and I would build the same five again:
- A snapshot mirror. Migration selection never queries the live production database. It reads a periodically restored snapshot, treated as eventually consistent. This protects production from analytical load and makes every batch reproducible: you can always answer "why was this subscription selected?"
- A selector that is the single source of truth for eligibility. Every rule about who can migrate right now lives in one place: country rollout, payment-method coverage, renewal-date windows, tax-address completeness, currency validity, minimum-amount floors, credential expiry. Downstream validators re-check the same rules, but only as backstops. The moment eligibility logic lives in runbooks or spreadsheets, you will migrate someone you should not have.
- Batching, locking, and one correlation ID. Each batch freezes its users' payment-method changes for the duration (a migration lock every downstream service respects), and carries a batch identifier stamped everywhere: database rows, logs, export files, and Stripe metadata. When something goes wrong at 2am, one ID traces a subscription through the entire pipeline.
- The re-tokenization exchange. The part everyone underestimates, covered in the next section.
- Cutover and reconciliation. Bulk import into Stripe creates subscription schedules pinned to each renewal boundary; a suspend-renewals call tells the old biller to stand down; and a reconciliation pipeline proves, continuously, that eligible = migrated + deferred + excluded, with nothing unaccounted for.
Re-tokenization: you cannot export your own cards
Card numbers live inside your provider's PCI scope. You, the merchant, never had them, so you cannot take them with you. What actually happens: after you sign a data-transfer authorization, Adyen ships tokens directly to Stripe over an encrypted channel. Your engineering work is around that handshake, not inside it: produce the export manifest, verify a sample of records end to end, and ingest the mapping file that comes back linking each old customer reference to a new Stripe customer and payment method.
And critically: each payment method migrates differently.
- Cards follow the full token-export handshake. Budget real calendar time: support tickets, legal wording, sample verification, and multi-day turnarounds between steps.
- Wallets (Apple Pay and friends) migrate as network tokens and arrive on Stripe as ordinary cards. Two traps: do not gate wallet eligibility on stored card expiry (the expiry you have describes the funding card, not the device token), and do not expect wallet-named objects on the Stripe side afterward. Downstream, a migrated wallet is just a card.
- PayPal-style billing agreements skip the exchange entirely. The agreement ID is portable, so this is a plain data import. Migrate these first: same platform, same reconciliation, none of the token-exchange risk.
- SEPA Direct Debit migrates mandates, not tokens: a takeover of the existing authorization, with its own reference and its own legal wording. The sharp edge: the new provider may email your customers that a "new mandate" is being created. Suppress that, or your support team meets a wave of alarmed emails about a change customers never requested.
Sequencing: the hold window runs the schedule
Stripe's bulk migration enforces a hold window: imported subscriptions must start far enough in the future (around a day, plus the operational headroom you add on top). That constraint, not your project plan, dictates batch composition. Any subscription renewing inside the window gets deliberately deferred: it renews once more on Adyen and joins a later batch. Splitting every batch into "eligible now" and "leftovers" is not an optimization. It is the mechanism that prevents double-charging on one side and missed renewals on the other.
The sequence that worked, easiest to hardest:
- Prerequisites: price catalog reproduced on Stripe, snapshot pipeline proven, reconciliation running against empty sets.
- Billing agreements (PayPal) — no token exchange, shakes out the whole pipeline.
- Cards — the main volume, in country-by-country waves.
- Wallets — riding the card machinery.
- SEPA mandates — smallest volume, most process-sensitive.
- Delta batches, forever. Failed records fixed and retried, newly eligible subscriptions, newly opened countries. The migration does not end with the big batches; it tapers through cleanup passes.
Reconciliation: count identities, not events
Three reconciliation lessons that each cost us real time:
Reconcile row states, not log events. Our first alarm compared distinct-subscription counts against log-event counts. One failure path logged twice, another logged nothing, and the alarm cried wolf for months over subscriptions that were never missing. The formula that works: eligible − exported = deferred + excluded + failed, every term computed from current row state, never from logs.
Your own database is a floor, not the truth. Other systems keep running during a months-long migration, and some of them delete or rewrite records you were counting on. The authoritative answer to "was this ever migrated?" has to come from the destination — Stripe's own data warehouse — which is only possible because of the next lesson.
Design the metadata before the first batch. Every object created on Stripe carried our batch ID, the internal subscription UUID, and the old Adyen reference. That metadata is what makes cross-provider reconciliation queryable at all, and it cannot be retrofitted onto objects you have already created. This is the single highest-leverage decision in the whole project.
The migration is 20% of the work. The long tail is 80%.
The happy path moves the bulk of the base. What remains is a long tail of subscriptions that cannot migrate yet: incomplete tax addresses, expired credentials, unsupported contract shapes, currencies invalid for a customer's residence country, amounts below a market's minimum charge, cancelled billing agreements. Individually small, collectively a real fraction of your book, and every record in it is paying revenue.
The approach that worked: give every exclusion a named bucket backed by a runnable count query, and export each bucket as a cohort file that owning teams can act on (address backfills, credential update campaigns, plan restructuring). A spreadsheet nobody can regenerate is a liability. A query that emits the current cohort on demand is an asset that outlives the migration.
Ten lessons I would hand any team doing this
- Snapshot pipelines need a write-back path. Our best bug: cancellations recorded by a downstream service never propagated back to the source system, so the next snapshot still showed those subscriptions as active — and the engine re-selected, re-locked, and re-abandoned them forever. Any terminal state must flow back to the system your snapshots come from.
- Sequence around the provider's hold window, not your calendar. Defer-and-renew-once-more beats clever timing every time.
- Every eligibility guard must be enforced by the selector, not just checked by the writer. Two state columns that can disagree will disagree, and the disagreement is a double-billing risk.
- Scope every enrichment to an explicit batch. A mapping file applied to "whatever is in flight" is corruption waiting for an out-of-order delivery. Scoped, the wrong file is a harmless no-op.
- Two similar-looking metrics are usually two different populations. Write down what each number counts, next to the number.
- A count moving the wrong way is a basis change, not a trend — usually failed records released back into the pool, or a backfill widening eligibility. Confirm the basis before reporting the delta.
- Re-enabling an excluded market is never one line. Country assumptions hide in eligibility rules and in export writers. Grep for every hardcoded list before touching the allowlist.
- Cache invalidation will eat a batch. Ours was a price catalog cached in process memory: new version in the database, stale version serving, an entire run erroring on missing prices. The redeploy is a numbered runbook step, not a footnote.
- Write the runbook for the person who gets paged. Prerequisites first, one correlation ID, recovery paths inline, and a table of known edge cases pointing at the exact code that enforces each one.
- Verify samples by hand at every provider handoff. A handful of records checked end to end — old reference, new token, last four digits — catches category errors that no aggregate count will.
How long does it take?
The honest answer: the engineering is measured in weeks; the migration is measured in months. The calendar is consumed by things you do not control — token-export turnarounds, legal authorizations, hold windows, renewal dates spread across the whole cycle — and by the long tail, which is limited by other teams' capacity to fix data, not by your code. Plan the project around the calendar, staff it around the engineering, and resist every temptation to compress the sequence. The migrations that go wrong are the rushed ones.
Leaving a different provider?
The framework above is universal; the credential transfer and the gotchas are provider-specific. I keep a guide per starting point: Braintree, PayPal, Chargebee, Recurly, NMI, Checkout.com, QuickPay, and the special case of moving between Stripe accounts. The Babbel engagement behind this guide is written up as a case study.
Frequently asked questions
How long does an Adyen to Stripe migration take?
Plan in months of calendar time, not weeks of engineering time. The engineering is weeks; the calendar is dominated by the provider-to-provider token export handshake, legal authorization, staged cutover waves around renewal dates, and the long tail of edge cases. Rushing the calendar is how migrations create incidents.
Can I export my own card data from Adyen?
No. Card numbers live inside the provider's PCI scope, so the merchant never touches them. Adyen ships tokens directly to Stripe through an encrypted transfer under a data-transfer authorization you sign. Your job is producing the manifests, verifying samples, and reconciling the mapping file that comes back.
Will customers notice the migration?
Done right, no. The new provider takes over exactly at each subscription's next renewal, so nobody is billed twice, nobody is skipped, and everyone keeps their payment method and billing day. The only customer-visible failure modes are the ones you design out: duplicate mandate emails, double charges from retry bugs, and gaps from cutover races.
What happens to SEPA mandates when migrating to Stripe?
Mandates migrate as a takeover, not as new mandates: the existing mandate reference transfers so the customer never re-authorizes. Two traps: mandate transfer follows a different process than card tokens, and the new provider may email customers about a 'new' mandate unless you explicitly suppress it.
Planning a migration? I run fixed-scope Stripe migrations end to end, and a 90-minute clarity call if you want your existing plan pressure-tested before you commit. Either way, the first conversation is about whether your migration is ready to run, not about selling you one.
Two minutes with the free migration readiness check tells you whether that conversation is worth having yet.
Written by Andrej Dragojevic, Stripe Certified Professional Billing Architect.