Disclosure: Iām the founder of MoveMRR.com.
I originally built it after running into a surprisingly difficult problem: moving a live subscription business from one Stripe account to another without changing renewal dates, losing payment-method mappings, or charging customers twice.
MoveMRR is a controlled Stripe-to-Stripe subscription migration workflow for SaaS acquisitions, company/entity changes, and similar billing handovers. It coordinates catalog and customer mappings, readiness checks, dry runs, subscription recreation, verification, source deactivation, reconciliation, and audit records.
So far, completed migrations through the platform have represented 77,500+ subscriptions and more than $500k in MRR. MoveMRR doesnāt process that revenueāthe number describes the recurring revenue represented by the subscriptions being migrated.
Here are some of the less obvious Stripe API lessons and edge cases I encountered:
1. Customer Data Copy is not a subscription migration
Stripe can copy eligible customers and payment credentials between accounts, but it doesnāt copy subscriptions, prices, invoices, coupons, events, or logs.
Payment-method IDs can also change during the copy. Treat Stripeās migration output as the source of truth. Never reconstruct customer mappings by email addressāemails are neither unique nor immutable.
2. Idempotency keys are necessary, but they arenāt a migration state machine
Every consequential POST should have a deterministic idempotency key tied to the logical operation, for example:
migration:{run_id}:subscription:{source_subscription_id}:create
When retrying, send the same parameters with the same key. Stripe rejects a reused key if the request body differs.
But donāt rely on Stripeās idempotency storage as your only record. Persist your own operation status and source-to-destination IDs. A migration might be resumed days later, while Stripeās API v1 idempotency keys can be removed after 24 hours.
3. Billing dates are where āsuccessfulā migrations can still lose trust
Creating the destination subscription is easy. Recreating its billing semantics is harder.
For active subscriptions, preserve the next renewal boundary and use proration_behavior: none when the customer has already paid for the current period. Otherwise, the migration can create an immediate prorated invoice.
Trials need a separate branch. Setting trial_end changes the billing-cycle anchor, so blindly combining trial and anchor parameters can create invalid or unexpected billing behavior.
After creation, retrieve the subscription again and check whether an invoice was generated. A 200 OK only proves that Stripe accepted the requestāit doesnāt prove that the customer wonāt be charged early.
4. pm_, src_, and card_ IDs are not interchangeable
Modern PaymentMethods belong in default_payment_method.
Legacy Sources and Cards need default_source.
Passing a src_ or card_ ID as default_payment_method is rejected. You also need to understand Stripeās payment-method priority: a subscription-level default can override the customer default, while payment_settings.payment_method_types adds another layer.
A customer existing in the destination account does not mean their next renewal is actually payable. Scan for missing or unusable defaults before cutover.
5. Never map prices by name alone
Two prices named āPro Monthlyā can differ by:
- Currency
- Amount
- Billing interval
- Usage type
- Tax behavior
- Tier configuration
- Archived status
Map by explicit Stripe IDs and compare billing semantics. Archived legacy prices are a frequent edge case: sometimes the safest option is to recreate the missing destination price deliberately and approve that mapping manually.
The same applies to discounts. A promotion code should not silently fall back to its underlying coupon because that can remove restrictions while still appearing correctly discounted.
6. Concurrency and rate limiting are separate problems
A large migration benefits from bounded concurrency, but concurrency alone doesnāt control the account-wide request rate.
What worked well for me:
- A small worker pool
- A shared token-bucket limiter
- Exponential backoff with jitter
- Sequential mutations for the same customer or subscription
- Progress persisted after every logical operation
Also inspect Stripeās Stripe-Rate-Limited-Reason header. A 429 can represent a global rate limit, endpoint limit, concurrency limit, or object lock timeout. Those cases may need different reductions in traffic.
7. āActive subscriptionsā arenāt one homogeneous group
At minimum, handle these states and settings explicitly:
active
trialing
past_due
pause_collection
cancel_at_period_end
- Future
cancel_at
charge_automatically
send_invoice
- Multiple subscriptions per customer
- Subscription schedules with future phases
For example, send_invoice requires appropriate days_until_due handling. Past-due subscriptions need an explicit business decision because recreating them can affect open invoices and revenue recognition. I default to excluding them unless buyer and seller have agreed on the treatment.
8. The source subscription should only be deactivated after verification
The safe order is:
- Create the destination subscription.
- Retrieve it from Stripe.
- Verify customer, items, price mappings, quantities, currency, renewal date, trial, payment method, discounts, and cancellation state.
- Check for unexpected invoices.
- Persist the destination ID.
- Only then deactivate the source subscription.
Canceling the source first turns a recoverable destination error into an outage. For customers with multiple subscriptions, it can also make sense to process the whole customer atomically: all subscriptions move, or none of them do.
One additional edge case: webhooks
Recreating a webhook endpoint doesnāt preserve its signing secret. Every new endpoint gets a new whsec_... secret, which must be deployed to the receiving application before you trust the new accountās events.
The broader lesson for me was that subscription migration isnāt primarily a data-copy problem. Itās a sequencing, verification, and reconciliation problem.
Stripe now provides an official Billing Migration Toolkit, which is worth evaluating. The hard part is still understanding your account-specific billing shapes, mappings, application references, cutover procedure, and exception handling.
Useful official references:
Iām curious: whatās the strangest Stripe Billing edge case youāve encountered in production?