Sari la conținut
← Înapoi la blog
payments · stripe · braintree · engineering4 min de citit18 aprilie 2026

Migrarea unui stack de plăți de la Braintree la Stripe — fără să pierzi un singur abonat

Un playbook testat în producție pentru a muta mii de abonați recurenți între procesatori tranzacțional, cu import idempotent, anulare automată în Braintree și un trail CSV pe care îl poți apăra în fața departamentului financiar.

If you Google "Braintree to Stripe migration" you will find dozens of marketing posts and exactly zero useful playbooks. This is not one of those posts. This is what we actually did to move thousands of monthly donors for an independent newsroom from PayPal/Braintree to Stripe — without losing one of them and without a billing complaint.

Why people botch this

A subscription migration is not a data migration. A data migration ends when the rows match. A subscription migration ends when the next charge succeeds at the new processor. Between those two moments live every interesting failure: customer mismatches, currency mismatches, mid-cycle billing dates, dunning state, "did this customer actually want to cancel last month?", and the unique pleasure of being charged twice.

The single biggest mistake teams make: they treat the migration as a one-shot CSV import. It is not a CSV import. It is a distributed transaction between two payment processors, a database, your application, and an email pipeline.

The model we used

We modelled the migration as a transactional pipeline with four idempotent stages, each with its own CSV input and output:

  1. Export & normalize — pull every active Braintree subscription, join it against the Stripe customer mapping, produce a single canonical CSV. Idempotent: re-running it should produce the same file (modulo new subscriptions).
  2. Stripe import with duplicate prevention — for every row, create the Stripe subscription if and only if no equivalent one exists. Use idempotency-key on every Stripe API call, derived from the Braintree subscription ID.
  3. Braintree cancellation, only on Stripe success — for every successful Stripe import, cancel the corresponding Braintree subscription in the same logical transaction. If the Stripe step succeeded, the Braintree step must eventually succeed; we retry on transient errors and surface persistent ones.
  4. Reporting — every success and every failure is appended to two CSV files. Nothing is silently lost. Finance can reconcile in Excel.

The non-obvious bits

The customer ID mapping is the contract. Maintain braintree_subscription_id → stripe_customer_id → stripe_subscription_id as a first-class table, not a CSV. The CSV is a snapshot; the table is the source of truth. Every retry re-queries the table.

Idempotency keys are not optional. Stripe will happily create a second subscription if you ask it twice. Use a deterministic key like migration_v2:bt_<subscription_id> so retries are safe.

Cancel last, never first. If you cancel Braintree before Stripe confirms the new subscription, you are one network blip away from charging your most loyal customer zero this month and waking up to a refund firestorm.

Don't run it as one big batch. Run it in waves of 50–200, with a human looking at the report between waves. The first wave will surface a class of errors you didn't predict. There always is one.

Save every payload. Store the full Braintree subscription JSON and the full Stripe response JSON next to the mapping row. When somebody six months later asks "why did we charge this customer €29.99 instead of €25", you have the answer in 30 seconds.

What you can copy

The architecture in plain English:

migration-orchestrator.js
├── braintree-to-stripe-csv-generator.js      → step 1
├── stripe-subscription-importer.js           → step 2 (idempotent, batched)
├── braintree-cancellation-service.js         → step 3 (only after step 2 succeeded)
└── csv-reporter.js                           → step 4 (success + error CSVs)

Add a --dry-run flag at every step. Add a BATCH_SIZE env var. Refuse to start if STRIPE_SECRET_KEY looks like a test key in a "production" run (and vice versa). Log every API call with its idempotency key.

What you absolutely cannot skip

A test environment with real Braintree sandbox subscriptions and real Stripe test customers. Migrate them first. Run the cancellation. Re-run the orchestrator. It must be a no-op the second time.

If the orchestrator is not safely re-runnable on a Tuesday morning while customers are loading their app, you are not done.

The result

Thousands of donors moved cleanly between processors. Zero billing complaints. Zero lost recurring revenue. The team kept their morning espresso.

If you're staring down a similar migration: the technology is the easy half. The hard half is admitting up front that this is a transaction across two systems you don't fully control, designing for retries from minute zero, and refusing to ship the orchestrator until it is safe to run twice in a row.

That's the whole post. There is no clever trick. There is only the discipline.

Continuă lectura