Cut Over to a New Payment Provider Without Losing Orders
For: Head of Engineering or CTO at a mid-size e-commerce or SaaS company processing 10K–200K transactions/month, who has already chosen a new payment provider and now cannot find an honest, sequenced plan for the actual cutover — not the integration docs, but the 72-hour window where both gateways are live, webhooks are firing from two sources, and one mis-sequenced step drops a live order silently
The riskiest moment in a payment gateway migration is not the switchover itself. It's the 30-day tail afterward, when the old provider keeps firing webhooks for refunds, disputes, and delayed captures on pre-migration charges that your new routing logic was never written to handle. Plan for the tail first, then work backwards to the cutover window. This playbook assumes you've already picked a provider, integrated it in staging, and now need an opinionated sequence for the live switch.
What follows is written for a Head of Engineering or CTO running 10K–200K transactions per month, with subscriptions, refunds, and disputes in the mix. If you're a checkout-only merchant with no recurring billing and no stored cards, you can skip about a third of this. Everyone else, read carefully.
The situation this applies to
You have:
- A live production gateway (call it OldPay) with active customers, stored payment methods, in-flight subscriptions, and open disputes.
- A new gateway (call it NewPay) fully integrated in staging, with webhooks tested against a tunnel.
- A plan to migrate that ends at “flip the flag” and says nothing about what happens when both providers are simultaneously firing events at your API.
If that's you, the seven steps below are the plan. Each has a checkpoint you can verify before moving to the next.
Step 1: Freeze the schema and add gateway-of-record to every payment object
Before you touch routing logic, your data model has to answer one question for every charge, refund, subscription, and dispute: which gateway owns this? If it can't, you will corrupt reconciliation state within the first hour of cutover.
Concretely:
- Add a
gatewaycolumn (enum:oldpay,newpay) topayments,refunds,subscriptions,disputes,payment_methods, and any ledger table. - Backfill everything to
oldpay. Every existing row is old-provider until proven otherwise. - Add a
gateway_event_idcolumn and a unique constraint on(gateway, gateway_event_id). This is your idempotency key for webhooks. Do not skip this. - Update every read path that touches provider APIs (issue refund, cancel subscription, fetch dispute evidence) to branch on
gateway, not on a global config flag.
Anti-pattern: A single “active_provider” config flag that flips globally. This forces every refund on an old-provider charge to go through your new provider, which will fail, and now your support team is manually processing refunds in two dashboards.
You'll know this step is done when: you can point at any payment record in production and, without looking at the created_at date, tell the system which API to call to refund it.
Step 2: Build a webhook router with strict source authentication
During the cutover window and for at least 30 days after, both providers will send you webhooks. Some events will be for the same underlying customer. Your webhook handler needs to be a router, not a switch.
Design:
- One endpoint per provider:
/webhooks/oldpayand/webhooks/newpay. Never a single shared endpoint. This makes source authentication trivial and lets you rate-limit and monitor independently. - Signature verification is mandatory on both. Reject unsigned or invalid-signature payloads with 401 before any parsing. Log the rejection.
- Every handler writes to a raw
webhook_eventstable first, with(gateway, event_id, payload, received_at, processed_at). Then a worker processes asynchronously. The HTTP response is 200 as soon as the raw row is written. - Idempotency: check
(gateway, event_id)before processing. Duplicates are common during cutover because providers retry aggressively when your app is under load.
Anti-pattern: Processing webhooks synchronously inside the HTTP request. During cutover you will see burst traffic, timeouts, provider retries, and duplicate processing. Async with idempotency is not optional at this volume.
You'll know this step is done when: you can replay the last 24 hours of production webhooks from either provider through the router and produce zero duplicate side effects.
Step 3: Reconcile OldPay to zero drift before you route a single new charge
You cannot debug a dual-gateway world if the single-gateway world is already broken. Most teams discover during cutover that their pre-migration reconciliation has been drifting for months — a few cents here, a stuck subscription there, a refund that fired in the dashboard but never updated the DB.
Run a full reconciliation against OldPay:
- Pull every charge, refund, and payout for the last 90 days via the OldPay API (not the dashboard export — the API is the source of truth).
- Diff against your database. Expect drift. Investigate every discrepancy, don't just null them out.
- Fix the root cause of any systemic drift — usually a webhook you dropped six months ago or a race condition on refund state.
- Get to zero drift, or a known and documented drift, before the cutover date. Not after.
You'll know this step is done when: your daily reconciliation job is green for seven consecutive days and any exceptions are individually explained.
Step 4: Route new charges to NewPay, but keep OldPay hot for everything else
This is the actual cutover, and it's the smallest, most boring step in the playbook. If steps 1–3 are done, this is a config change.
The rule for the cutover window (call it T-0 to T+72h):
- New charges (new checkout, new subscription sign-up): route to NewPay.
- Existing subscriptions: stay on OldPay until their next renewal, at which point they either (a) renew on OldPay one last time and migrate the token, or (b) get proactively migrated via a card-on-file token migration (see step 5).
- Refunds: route based on the
gatewayfield of the original charge. Always. No exceptions. - Disputes and chargebacks: whichever provider surfaced them handles them. You will be managing two dispute queues for months. Accept this now.
Ship this behind a percentage rollout. Start at 1% of new charges to NewPay for the first two hours. Watch authorization rates, latency, and webhook throughput. Ramp to 10%, then 50%, then 100% over the first 24 hours. If NewPay's auth rate is materially lower than OldPay's on your traffic mix, you'll see it at 10% and can roll back cheaply.
Anti-pattern: Big-bang 100% cutover on a Friday night because “traffic is lower.” Traffic is lower and so is your on-call bench. Do the cutover on a Tuesday morning when the whole team is at their desks.
You'll know this step is done when: 100% of new charges are on NewPay, OldPay is still processing renewals and refunds correctly, and your dashboards show both gateways with expected volumes.
Step 5: Migrate stored payment methods deliberately, not opportunistically
If you have card-on-file or stored payment methods, this is where most migrations quietly fail. There are two approaches and you should pick one, not blend them.
Option A: Bulk token migration. Most major providers support importing tokenized card data from another PCI-compliant provider. This is a formal, PCI-DSS-governed process: you request it from NewPay, they coordinate with OldPay, and you receive tokens for your existing cards on NewPay. Every subscription can then be re-pointed. This is the cleanest option and the one I'd default to if your provider supports it.
Option B: Migrate on next successful charge. Leave existing subscriptions on OldPay. On next renewal, if the charge succeeds, use the returned card data to create a NewPay payment method for future charges. This works but takes a full billing cycle to complete, extending your dual-gateway tail from 30 days to 60–90.
Whichever you pick, track migration state per payment method: migration_status in {not_started, token_received, verified, failed}. Do not mark a method as migrated until you've successfully authorized against it on NewPay — a $0 auth or a low-value real charge. Bulk-imported tokens sometimes fail on first use for reasons neither provider explains.
Anti-pattern: Trusting that a bulk token import “worked” because the CSV was accepted. Verify with a live authorization.
You'll know this step is done when: every active subscription has a verified NewPay payment method and a successful renewal on NewPay, and OldPay's active subscription count is zero.
Step 6: Plan the 30-day tail as an explicit project, not a “we'll monitor it”
This is the step everyone skips and everyone regrets. After you're on NewPay for new charges, OldPay will keep firing webhooks for:
- Delayed captures on charges authorized before cutover.
- Refunds processed by your support team on old charges.
- Disputes and chargebacks, which can arrive up to 120 days after the original charge.
- Subscription events for any subscriptions you didn't proactively migrate.
- Payout events for money OldPay still owes you.
- Fraud review outcomes for charges flagged before cutover.
Your new routing logic almost certainly wasn't written to handle these gracefully because the developer writing it was thinking about NewPay. Common failure modes I've seen in this window:
- A refund webhook from OldPay hits the router, gets misclassified because the payment lookup only checks NewPay, and the order stays marked as paid in your DB while the customer's card is credited.
- A dispute webhook from OldPay arrives 60 days post-cutover; your dispute handler was rewritten for NewPay's payload shape and silently drops it. You find out when you lose the dispute by default.
- A delayed capture succeeds on OldPay a week after cutover for a pre-order; your inventory system already released the reservation because it was expecting a NewPay capture event.
Explicit tail-management actions:
- Keep OldPay's webhook endpoint fully live and monitored for at least 90 days. Alert on any drop in webhook volume — that's how you catch OldPay accidentally being disabled.
- Build a “legacy webhook” dashboard: count of events by type, per day, from OldPay. Volume should decay predictably. A spike means something's wrong.
- Explicitly write handlers for every OldPay event type that might arrive post-cutover, even if the handler is just “update the payment row and notify support.”
- Do not decommission OldPay API keys until you've had zero events for 30 consecutive days and your finance team confirms the final payout has landed.
You'll know this step is done when: OldPay's daily webhook volume is zero for 30 straight days, all disputes are closed, all payouts are received, and finance has signed off on final reconciliation.
Step 7: Reconcile daily against both providers until the tail closes
Your existing reconciliation job needs to become two jobs, or one job with a gateway dimension. Every day, for every gateway, compare provider-side truth to your database. Alert on any variance above a defined threshold (I'd start at zero and only raise it if you're drowning in noise).
What to reconcile:
- Charge count and sum, by day, by gateway.
- Refund count and sum, by day, by gateway.
- Dispute open/closed counts, by gateway.
- Payout amounts and arrival dates.
- Subscription state (active/canceled/past_due) — expect this to drift the most.
If you're building this from scratch under time pressure, teams that have shipped fintech products before — the kind of work our GimBooks accounting SaaS and Cashpo lending platform case studies get into — usually already have reconciliation harnesses that can be adapted rather than written cold. Adapting is faster than greenfield.
You'll know this step is done when: reconciliation is green on both gateways for 30 consecutive days.
Failure modes I've seen
A short list of things that have actually gone wrong on real cutovers, in rough order of frequency:
- The silent refund. A support agent issues a refund in the OldPay dashboard because that's still where the charge lives. The webhook fires. Your handler doesn't recognize the event because it's routing on the new provider's schema. The customer is refunded; your DB says the order is still paid; your ledger drifts.
- The double-charged subscription. A subscription gets migrated to NewPay but the OldPay subscription isn't canceled first (or the cancel fails silently). Next renewal, both providers charge the customer. Support finds out from the customer, not from monitoring.
- The disappearing dispute. A chargeback arrives on OldPay 45 days post-cutover. Handler was deprecated. Deadline to submit evidence passes. Dispute lost by default.
- The webhook storm at cutover. Both providers retry aggressively when your app slows down under cutover load. Synchronous handlers time out, causing more retries, causing more slowdown. Async processing with idempotency is the fix, but only if you built it before cutover.
- Auth rate cliff on NewPay. NewPay's auth rate on your specific traffic mix (geography, card types, MCC) is 2–4 points lower than OldPay's. You don't notice at 10% traffic because absolute numbers are small. At 100% you're leaving real revenue on the table. Percentage rollouts catch this; big-bang cutovers don't.
- Timezone mismatch in reconciliation. OldPay reports in UTC, NewPay in your account's local timezone (or vice versa). Daily totals never match by exactly one day's worth of edge transactions. Fix the reconciliation query, not the data.
None of this is exotic. All of it is preventable if you plan the tail before you plan the cutover.
Frequently Asked Questions
How long should I run both payment gateways in parallel?
Plan for a minimum of 90 days of dual-gateway operation after the cutover, driven by chargeback windows (typically 120 days from the original charge) and subscription cycles. Don't decommission OldPay API access until you've had 30 consecutive days of zero webhook events and finance has confirmed the final payout. Rushing this is the single most common cause of post-migration reconciliation nightmares.
Can I migrate stored cards without asking customers to re-enter them?
Usually yes, if both providers are PCI-DSS Level 1 compliant. Major providers like Stripe, Adyen, Braintree, and Checkout.com support formal card token migrations from each other — it's a paperwork-heavy process governed by PCI rules, not a self-serve feature. Contact your new provider's onboarding team early; the process can take weeks to arrange before you can even begin.
What's the safest day and time to run a payment cutover?
Tuesday or Wednesday morning in your primary business timezone, with the full engineering and finance team on-call and awake. Avoid Fridays (weekend on-call risk), Mondays (accumulated weekend traffic anomalies), and month-end / quarter-end (subscription renewal spikes and finance close). Ramp traffic to NewPay in percentage stages over 24 hours, not in a single flip.
How do I handle refunds on charges that were made on the old provider?
Always route refunds based on the gateway that processed the original charge, stored as a gateway field on the payment record. Never route refunds based on a global “active provider” flag. Your refund UI, whether in an admin panel or API, needs to branch on that field for as long as any refundable OldPay charge exists — typically 6–12 months after cutover.
What does a payment gateway migration cost and how long does it take?
It depends heavily on transaction volume, whether you have stored cards and subscriptions, how clean your current reconciliation is, and which providers are involved. For a realistic scope and sequenced plan for your specific stack, contact CodeNicely for a personalized assessment — a proper estimate needs to look at your actual payment surface area, not a generic template.
Found this useful? CodeNicely publishes engineering and product playbooks weekly. Browse the archive or tell us what you're building.
_1751731246795-BygAaJJK.png)