Moving live subscribers from PayPal to Stripe without breaking their billing dates

A subscription migration looks like a data problem and turns out to be a consent problem. The rows are easy: customer, plan, amount, next billing date. The hard part is that you are moving a standing permission to charge someone, and that permission lives with your old processor rather than with you. Get the order of operations wrong and subscribers get billed twice in six weeks, billed on a day they never agreed to, or quietly stop being billed at all.

Having run this for a retained client — their live subscriber base moved from PayPal onto Stripe subscriptions — the question that decides the whole project gets settled before anyone writes import code.

Do you actually have a payment method to move?

Stripe will import card data from a previous processor. You build and test your Stripe integration first, submit a data migration request, then the old processor sends customer and payment data to Stripe — PGP-encrypted, over Stripe’s SFTP — and Stripe creates Customer objects with Card or PaymentMethod objects attached. Stripe says it typically imports within 10 business days of receiving correct data, and that the old processor may take days or weeks to produce it. You then get a file mapping old IDs to new Stripe IDs.

All of that assumes there is a card number in a vault that someone will hand over. With PayPal, scrutinise that. If your subscribers paid by PayPal wallet — a billing agreement against their PayPal account — the funding instrument is the account, not a PAN you can export. There may be nothing to import. Stripe’s guidance on requesting data from a current processor doesn’t name PayPal or commit to what any provider will release, so settle “can we get the vault?” in writing before planning a timeline around it. If the answer is no, this is a re-collection project, which changes comms, expected drop-off and how long you dual-run.

Nor does PayPal necessarily follow you to Stripe as a payment method. PayPal processed through Stripe is available only to Stripe accounts in a specific list of European countries, and recurring payments on it may require approval. Elsewhere, Stripe offers PayPal as an access-gated custom payment method where you host a Stripe-written adapter and process through your own PayPal account; confirm with Stripe what it supports for recurring collection before planning around it. For a US merchant, assume recurring collection will be card or bank debit.

Preserving the anniversary

Say you have payment methods in Stripe and a subscriber whose cycle started September 1, next charge due November 1. It is October 15. Create a plain subscription today and Stripe anchors the cycle to today, invoices immediately, and that subscriber pays twice in six weeks.

Three parameters on POST /v1/subscriptions do the work. backdate_start_date sets the historical start date. billing_cycle_anchor is a Unix timestamp fixing the reference point for future periods — day of week for weekly intervals, day of month for monthly and yearly, month of year for yearly. proration_behavior decides what happens to the gap between start and first full invoice.

Stripe documents this exact case: set backdate_start_date to September 1 and billing_cycle_anchor to November 1. But by default that combination immediately issues a prorated invoice covering September 1 to November 1 — precisely the double charge you were avoiding, since PayPal already collected for that period. For a migration you almost always want proration_behavior=none, which sets start_date to the backdated date and charges nothing for elapsed time.

Two details are easy to miss. Billing mode: since API version 2025-04-30, classic mode produces a single prorated line item for the backdated stretch, flexible mode one per natural billing period inside it. Stripe warns that proration_behavior=none under flexible mode records no debit for those periods, so later updates or cancellations won’t generate credit prorations against them.

And calendar days: for monthly and yearly prices, billing_cycle_anchor_config with day_of_month handles short months and leap years, and day_of_month=31 means “last day of the month” rather than “skip February”. It does not support anchoring on a backdated start date, so a true migration still needs the raw timestamp — in UTC, where a 5pm Eastern creation lands at 10pm on a different calendar day.

One hard limit: where the next billing date is more than 30 days out, the plain Subscriptions API won’t start a monthly subscription that far ahead. Stripe’s recommendation for imports is the Subscription Schedules API with start_date and default_settings[billing_cycle_anchor]=phase_start, which also lets you review the import before anything bills.

The traps

Coupons get eaten by the backdated period. A coupon’s duration counts from the backdated start date, not from the API call. Backdate three months with a duration_in_months=2 coupon and the discount is consumed before the first real invoice and removed. If a discount must survive, its duration has to exceed the backdated stretch.

The lag window. Between your old processor snapshotting the data and Stripe finishing the import, any card a customer updates on the old system is lost. You need a self-migration path for those customers: create the Customer, attach the payment method, set invoice_settings[default_payment_method], then create the subscription.

Turning off the old biller. Nothing in Stripe stops PayPal from charging. Stripe’s migration guidance ends with confirming the previous processor has cancelled all automatic billing — do that per subscriber, with evidence, and reconcile both sides for a full cycle. This is the most common way subscribers get billed twice.

Declines on first charge. Imported cards on file must carry off_session for merchant-initiated charges; get that wrong and issuers unable to confirm consent to recurring billing may decline, and the data can become ineligible for card account updater and network tokens. Stripe also cautions against mass-retrying after a migration — a burst of retries across many cards looks suspicious to issuers. expired_card points at stale vault data and responds to account updater; do_not_honor more often reflects a changed statement descriptor.

Re-runs. Key every create call by your own subscriber ID using Stripe’s idempotency header. Import scripts get re-run, and a re-run without idempotency keys is how one subscriber ends up with two active subscriptions.

Before any of it touches live mode, run the import in a sandbox with test clocks and advance past the first renewal for a representative sample: a mid-cycle monthly, an annual with months remaining, one anchored on day 31, one with a live coupon, one with a card expiring inside the window. Price IDs differ between sandbox and live mode, so remapping gets exercised too.

The trade-off worth naming

Vault import preserves revenue continuity and asks nothing of subscribers, but it depends on a third party’s cooperation on a timeline you don’t control, carries a lag window, and inherits whatever quality problems the old vault had. Re-collection is slower, costs you some subscribers outright and needs a comms campaign, but leaves you with fresh, correctly-consented payment methods and no dependency on anyone else’s export schedule.

Which is right depends on how much of your base is wallet-funded rather than card-funded, and how reachable those subscribers are. The mistake is choosing before you know — then discovering mid-project that half of them never had an exportable card at all.

Subodh Srivastava is the founder of SubcoDevs, a payments consultancy specialising in Stripe Connect marketplaces, subscription billing and migrations, dispute reduction and processor fee optimisation.