> ## Documentation Index
> Fetch the complete documentation index at: https://docs.integrateledger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Stripe Billing

> Move customers you already bill with Stripe subscriptions onto Ledger with one command, without re-subscribing anyone or charging them.

If you already sell monthly subscriptions with Stripe, you can put Ledger on top of them. Each customer keeps their subscription, card and renewal date. Ledger grants their plan's credits for the current period now, and the credits for every later period when Stripe renews.

## Connect your Stripe account

Ledger has to work in the Stripe account your subscriptions live in. On the dashboard's Developer page, connect that account's secret key: `sk_test_…` in Sandbox, `sk_live_…` in Live. Ledger creates a webhook endpoint in your account; check that it shows as healthy, since every renewal's credits arrive through it.

Try the whole migration in Sandbox first, with test-mode copies of a few customers.

## Run one command

From your app's repo, with a Sandbox API key in `LEDGER_API_KEY` (or saved with `ledger login`):

```bash theme={"system"}
npx integrateledger-cli migrate stripe
```

It reads your Stripe account (nothing changes yet) and:

1. **Proposes a catalog.** One paid plan for each monthly USD Price your customers are on, named after its Product and priced like it. It asks each new plan's monthly credits, and a feature with its credit cost if your `ledger.catalog.json` has none. A plan already in your config keeps its credits.
2. **Shows a report.** Each plan, each customer it will import with their next renewal date, and each subscription it will skip, with the reason (see [what can be imported](#which-subscriptions-can-be-imported)).
3. **Asks before changing anything.** Then it writes the plans to `ledger.catalog.json`, publishes the catalog, and imports each customer, one progress line each.
4. **Ends with a summary.** Imported, already imported, skipped by reason, failed, and the range of next renewals.

```text theme={"system"}
Sandbox · workspace · https://api.integrateledger.com · key from LEDGER_API_KEY

Plans
  pro  $49/mo  2000 credits/mo  new  price_1Pro… · 2 to import
  starter  $19/mo  500 credits/mo  new  price_1Sta… · 2 to import

Import 4
  user_di → pro  sub_1… · renews 2026-12-07
  …

Skip 2
  cus_Fay…  sub_1…  unsupported_subscription (discount)
  cus_Ed…   sub_1…  unsupported_subscription (not_monthly)

[1/4] user_di → pro  imported, renews 2026-12-07
…
Imported 4, already imported 0, skipped 2, failed 0.
Next renewals: 2026-12-07 to 2026-12-07. Ledger grants each one's credits when Stripe renews it.
```

| Flag | Use it to |
| - | - |
| `--dry-run` | See the report only. |
| `--customer-id user_id` | Use your own customer ID from that Stripe customer metadata key. By default the Ledger customer ID is the Stripe customer ID (`cus_…`). Your app passes the same ID to check and track. |
| `--credits price_…=1000` | Give a new plan's monthly credits without the prompt (repeatable; for CI and agents). |
| `--feature generate=1` | Add a feature when the config has none (repeatable). |
| `--yes` | Skip the confirmation. |
| `--live` | Migrate Live. You type `live` to confirm, or pass `--yes`. |

**Safe to rerun.** Every step is idempotent. Progress and your answers are saved in `.ledger/migrate-stripe.sandbox.json` (gitignored), so after a failure or an outage you run the same command again: it asks nothing, skips the customers already imported, and retries the rest. It exits `1` when any customer failed.

The command carries no usage over: each customer starts the current period with the plan's full credits. To carry over what they've used, see [Other ways](#other-ways).

## Other ways

> [!NOTE]
>
> `ledger migrate stripe` is the way to migrate. If you need to script it yourself, for example to carry over usage with `startingBalance`, the command is built on calls you can make directly: [Preview a Stripe migration](https://docs.integrateledger.com/api-reference/migrate/stripe-preview.md) (`client.previewStripeMigration()`) returns the same report as JSON, then for each customer [create](https://docs.integrateledger.com/api-reference/customers/create.md) them, [link their Stripe customer](https://docs.integrateledger.com/api-reference/customers/stripe-customer.md) and [import their subscription](https://docs.integrateledger.com/api-reference/billing/import.md) onto a plan with the same monthly price. Every call is safe to repeat.
>
> The [onboarding skill](https://docs.integrateledger.com/ai.md) runs the command for you when it finds Stripe subscriptions in your app. Connecting Stripe with OAuth and migrating in one click from the dashboard is planned next, on the same calls.

## Customers on a free plan

Customers on a free plan don't need an import. Create them and [attach](https://docs.integrateledger.com/api-reference/billing/attach.md) the free plan. You can still link their Stripe customer, so a later upgrade uses the card they already saved.

## What changes in Stripe

Ledger only changes two things in your Stripe account:

* **Metadata.** The customer and subscription get `ledger_organization_id` and `ledger_env`, and the customer also gets `ledger_customer_id`. Your own metadata keys stay as they are.
* **The subscription's Price.** Its item moves from your Price to the Ledger plan's Price, which has the same amount, with no proration. Nothing is charged, no invoice is created (a trialing subscription gets a \$0 one), and the renewal date stays the same. From the next renewal on, the invoice line shows the Ledger plan's name.

Ledger needs the subscription on its own Price because it grants credits only for invoices it can verify against the plan. Your old Price stays in your account, unused by the imported subscription.

## Which subscriptions can be imported

Ledger imports active subscriptions, subscriptions set to cancel at the period end (the plan ends when Stripe ends the subscription), and trialing subscriptions (the trial becomes the plan's first period, with its credits).

It refuses, without changing anything, a subscription it couldn't bill correctly, and says why. The command reports these as skipped:

| Error | What to do |
| - | - |
| `subscription_past_due`, `latest_invoice_unpaid` | Collect or void the open invoice in Stripe, then import. |
| `subscription_incomplete` | The first payment never succeeded: let the customer pay, or cancel it. |
| `subscription_ended`, `subscription_not_billable` | A canceled, expired, unpaid or paused subscription has nothing to import. |
| `multiple_subscriptions` | Ledger keeps one plan per customer. Consolidate the customer's subscriptions in Stripe first. |
| `stripe_customer_owned_elsewhere` | The customer's Ledger metadata names another Ledger workspace or environment. |
| `unsupported_subscription` | The `reason` names what Ledger can't bill yet: several items or a quantity above one, a price that isn't the plan's monthly USD amount, a yearly (`not_monthly`) or metered price, invoices sent for manual payment, a coupon (`discount`), a subscription schedule or pending update, paused collection, billing thresholds, pending invoice items, or a customer balance. |

Fix the cause in Stripe and run the command again; it picks up the newly importable customers.

## Live

Migration works the same way in Live, with your Live API key and `sk_live_` connection: `ledger migrate stripe --live`. It doesn't need Ledger's approval for live charges, because it starts no charge: your customers keep paying what they already paid, on the same dates. A later upgrade through Ledger is a new charge, and does need it.

See [Import a Stripe subscription](https://docs.integrateledger.com/api-reference/billing/import.md) and [Link a Stripe customer](https://docs.integrateledger.com/api-reference/customers/stripe-customer.md) for every field and error.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.