Skip to main content
If you already have customers paying you through Stripe, you don’t need to migrate them off Stripe to use BillingOS. BillingOS treats Stripe as the source of truth — your existing subscriptions keep running. The import flow simply mirrors them into BillingOS so the SDK can power portal, checkout, upgrades, downgrades, cancellations, and feature gates for those customers immediately.

What “migrating” actually means

Nothing moves off Stripe. Specifically:
  • Your existing Stripe customers keep paying through the same Stripe subscriptions they’re already on.
  • Charges, invoices, and payouts continue exactly as before.
  • Stripe remains authoritative for products, prices, customers, and subscriptions.
What BillingOS imports is a mirror of that state — plus the missing piece Stripe doesn’t have: the mapping from your internal user IDs to those customers, and the features each subscription unlocks in your app.

Before you start

You’ll need:
  • A BillingOS organization with Stripe connected (OAuth, production).
  • The BillingOS products you want to map your Stripe products to. Create them under Products in the dashboard first — the import wizard will ask you to match Stripe products → BOS products.
If you only have one or two pricing tiers, create the BillingOS products with the same features you offer today, then come back to run the import.

Step 1 — Run the import wizard

1

Open Settings → Import Data

Once Stripe is connected, you’ll see a new Import Data tab in your organization settings.
2

Preview

Click Start import. BillingOS scans your connected Stripe account and shows the totals:
34  Customers
 4  Products
 9  Prices
30  Subscriptions
3

Map products

For each Stripe product, choose the BillingOS product it corresponds to. The wizard shows the count of active subscriptions per Stripe product so you can prioritize the important ones.
Subscriptions whose Stripe product isn’t mapped will be skipped. You can re-run the import later after adding missing mappings — all imports are idempotent.
4

Click Migrate

The job runs in the background. You’ll see live progress (customers + subscriptions). For most accounts it completes in seconds; large accounts (10k+ subs) take a few minutes.

Step 2 — Pass email when you create session tokens

This is the trick that makes everything “just work” for your existing customers. When your backend creates a session token, pass the user’s email along with their ID:
const { sessionToken, expiresAt } = await billing.createSessionToken({
  externalUserId: user.id,
  email: user.email, // ← the magic
});
The first time any of your existing users hits the SDK, BillingOS looks up the imported customer row by email and binds it to your user ID. From that moment on, the customer is fully resolved — portal, checkout, feature gates, everything works.
No backfill, no CSVs, no extra API calls. The binding happens once per customer, transparently, the next time they touch your app.

Step 3 — Resolve “stuck” customers (if any)

After the import completes, BillingOS classifies un-bound customers into two buckets:

Pending

Customers with a unique email. They’ll auto-bind on first SDK session. No action required.

Needs attention

Customers with no email, or sharing an email with another customer in Stripe. These need a one-time manual bind.
If you have any in the “Needs attention” bucket, click Resolve on the import summary card to open the drawer. You can bind each one inline by pasting the matching user ID from your database.

Bulk-resolving from your backend

For larger sets, use the Node SDK to script the binding:
import { BillingOS } from "@billingos/node";

const billing = new BillingOS({
  secretKey: process.env.BILLINGOS_SECRET_KEY!,
});

// 1. Pull customers that aren't bound yet.
const { customers } = await billing.listUnresolvedCustomers();

// 2. Look each one up in your own DB and bind.
for (const c of customers) {
  if (!c.email) continue;
  const user = await db.users.findByEmail(c.email);
  if (user) {
    await billing.bindCustomer(c.id, user.id);
  }
}
External IDs are immutable once set. Double-check before binding — there’s no unbind operation.

What happens next

Once a customer is bound (auto via email, or manually), the following work for them with no extra code:
  • Portal — they can view their current plan, update payment method, cancel
  • Checkout — they can upgrade or downgrade through the BillingOS pricing table
  • Feature gatesuseFeature("api_calls") returns the right entitlement
  • Usage trackinguseTrackUsage records consumption against their subscription
All while their payment still flows through the same Stripe subscription they were on.

Re-running the import

The import is fully idempotent. Re-run it any time you:
  • Add new Stripe products and need to map them
  • Have new customers in Stripe that weren’t imported on the first pass
  • Migrate from another billing tool that ends up creating Stripe customers outside BillingOS
Click Re-run import on the completed-import card. Existing rows are updated in place; new rows are inserted.

FAQ

No. Their subscriptions stay on Stripe and continue charging exactly as before. The only code change is passing email to createSessionToken so they auto-bind on first session.
The import only ingests active-ish subscriptions (active, trialing, past_due, unpaid). Canceled and incomplete-expired subscriptions are skipped to keep the BillingOS state clean.
Not in v1. The import flow assumes a single connected Stripe Connect account per organization.
The import only mirrors customers that exist in your Stripe account. Free-tier users that you track outside Stripe (e.g., in your own database) keep working as before — they’ll get a BillingOS customer record the first time they hit your app with the SDK.
No. Imports happen in the background and don’t touch their Stripe subscriptions. There’s no double-charge, no payment retry, no email — your customers won’t know anything happened.