Choosing billing currencies before your first subscription
Which billing currencies to enable before your first customer: why a Stripe subscription currency is locked per customer, and what adding one later costs you.
In short
Enable only the currencies you are willing to keep. A workspace locks to one billing currency on its first subscription, and Stripe will not move a customer off it while a subscription, quote or discount still pins it. Adding a currency is easy; every plan version you publish afterwards carries a price in it.
Enabling a second billing currency is one more string in a PATCH body. Getting rid of it takes a conversation with every customer who subscribed in it. That asymmetry is the whole of this decision, and it sits at step one of the Get Started checklist for a reason: the currencies you save before you build any plans constrain every price you can create afterwards.
What you are building
An organization that bills in exactly the currencies you intend to support for the next few years, set before a single customer subscribes. When it works, your plan list carries one price per currency you enabled, and a workspace that subscribes gets stamped with its currency for good. No support email ever arrives asking "can you move my account to euros".
Before you start
You need an organization and your own Stripe keys connected. You do not need any plans yet, which is the point: do this before you build them, because a plan version has to carry a price in every currency your organization bills in.
Step 1: find out what you can actually charge in
Ask the API rather than guessing from a list you remember:
curl https://api.console.buildbase.app/api/organizations/settings/supported-currencies \
-H "Authorization: Bearer 665f1a2b3c4d5e6f7a8b9c0d:your-token-secret"{
"supportedCurrencies": [
"usd",
"eur",
"gbp",
"cad",
"aud",
"jpy",
"inr",
"chf",
"sek",
"nok",
"dkk",
"sgd",
"hkd",
"nzd",
"mxn",
"brl",
"pln",
"czk",
"thb",
"try",
"zar"
]
}That is 21 currencies, and the number comes from SUPPORTED_BILLING_CURRENCIES in packages/shared, the same constant the API validates against. Anything outside this list is refused at save time, so read it before you promise a price to someone.
Step 2: decide how many, and what the second one does to checkout
Pick the currencies you have already been paid in. Then look at what the second one does to the code that creates subscriptions, because that is where the cost lands first.
A subscription checkout resolves its currency in this order:
- The workspace's own
billingCurrency, if it has one from an earlier subscription or credit purchase. - An explicit currency on the request.
- The currency on the plan version's first price variant.
- Your organization's first billing currency, as a last resort.
With one currency enabled, steps three and four can never disagree with each other. Add a second and price a plan in both, and the branch above them takes over: a plan version carrying more than one variant with no currency on the request does not quietly fall back to anything. It throws.
Plan version has multiple currencies. Please specify currency (e.g. usd, cad) when creating checkout.
So the real consequence of a second currency is not a settings row. It is that every call site creating a checkout now has to decide a currency and say so, and the one you forget fails at the moment a customer tries to pay.
Step four is worth knowing for a different reason: you do not choose which currency sits first. The handler de-duplicates and sorts the list before storing it, so an organization selling in usd that adds aud gets ["aud", "usd"] back, and Australian dollars become the last-resort currency. On a subscription that rarely decides anything, because a price variant almost always carries its own currency. On a credit-pack purchase it decides more often: that path consults the org list before the variant, so the alphabetically-first code is the one a credit purchase falls back to.
As for how many: fewer than the form tempts you into. BuildBase ships support for 21 currencies and sells its own plans in one, at $49, $99 and $199 a month. Twenty-one is a capability. One is a pricing decision, and they are not the same number.
Key takeaway
Every currency you enable is a price that every plan version you publish afterwards has to carry, once for every billing interval you offer. A seventh currency added in month nine is a fresh variant on each version you ship from then on, carrying a price for every interval you sell.
Step 3: save the list
curl -X PATCH https://api.console.buildbase.app/api/organizations/settings/config \
-H "Authorization: Bearer 665f1a2b3c4d5e6f7a8b9c0d:your-token-secret" \
-H "Content-Type: application/json" \
-d '{"billingCurrencies": ["usd"]}'Or set it on the organization settings page in the console, which is what the Get Started checklist links to.
Now the part worth reading twice. That PATCH merges. It never removes. The handler takes what you already hold, adds what you sent, de-duplicates and sorts, and stores the union. Send ["eur"] to an org holding ["usd"] and you now bill in both. There is no removal route, and the code comment says so plainly: the PATCH never removes one.
So the result of this step is a list you live with: the only way to shorten it is a migration, not a setting.
Step 4: price a plan version in every currency you enabled
Publishing a plan version runs a guard that checks it has a pricing variant for every currency your organization bills in. The bar is set by the org's current currency list, not by the version you copied from, so a currency added after v1 shipped is a currency v2 has to carry before it will publish. Changing pricing without affecting existing customers covers the versioning mechanics around that guard.
You should see a publish refused with a missing-variant complaint until every enabled currency has a price. That refusal is the currency list catching up with you.
Step 5: watch a workspace lock
Subscribe a demo workspace, then read it back. Its billingCurrency is now set, and it is set once: the service that writes it only does so when the field is empty.
// server-side, on first subscription
if (workspace && !workspace.billingCurrency) {
await workspaces.updateById(orgId, workspaceId, {
billingCurrency: currency,
});
}The code comment above that function gives the reason in one line: Stripe allows one currency per customer. A credit purchase sets the same field, so a workspace can be locked by buying a credit pack before it ever subscribes to anything.
The failure modes
"At least one currency is required (e.g. usd, cad)." You sent an empty array hoping to clear the list. Merging an empty array into an empty list leaves nothing, and an org that bills in no currency cannot price anything, so the save is refused. It is what you get for reaching for the settings API as a removal tool.
"Unsupported currency codes: CNY. Only these are allowed: USD, EUR, ..." A code outside the 21. The message lists the full permitted set, so paste it and pick again.
A refused PATCH when you are on a smaller plan. The number of currencies an org may hold is itself a plan limit, and where that limit is enforced it is measured on growth only. A request that adds nothing to what you already hold passes through untouched. That matters if you downgrade. An org can legitimately hold more currencies than its current plan allows, because nothing can take one back, and refusing the no-op would trap it.
"You cannot combine currencies on a single customer." This one is Stripe's, not ours. It is the end state this guide exists to avoid. You tried to subscribe a customer who already has a default currency to a price in another one.
Why Stripe makes this permanent
A Stripe Customer has one default currency. It is set by the first thing that needs one, and currency is not a writable field on customer create or update, so you cannot simply correct it later.
While any of these exists in the old currency, the customer stays pinned to it:
- active subscriptions, meaning any status other than canceled
- not-started or active subscription schedules
- discounts
- draft or open quotes
- pending invoice items, the ones not yet attached to an invoice
Stripe's documented position on the subscription half is flat: you can't have two active subscriptions with different currencies.
Moving a customer anyway is a real procedure, not a toggle. Create the products and prices in the new currency. Void the open discounts, invoice items, invoices and quotes sitting in the old one. Convert any customer balance with balance transactions, because a credit balance is held per currency and a CAD invoice will not spend a USD credit. Cancel the old subscription with prorations disabled, then create the new one using the previous subscription's period end as the billing cycle anchor so the customer is not billed twice for one month.
The parameter names for the proration and anchor behaviour are in Stripe's API reference rather than in the migration article, which carries no code at all. (Stripe behaviour verified 12 October 2026.)
Not every customer is stuck, and anyone testing this will find that out fast. Once every subscription, schedule, discount, quote and pending invoice item in the old currency is gone, the next billing object in a new currency re-sets the default. So the permanence is really about customers with live billing, which is to say customers who are paying you.
What multi-currency prices do and do not fix
Stripe supports several currency options on one Price object, which is the documented way to sell in several currencies while each customer stays on one. Use it. It still does not hand you a switch for an existing customer.
Two details decide how it behaves. All your prices must share the same default currency. And if you create subscriptions directly rather than through Checkout, the extra currencies do nothing until you pass the currency parameter, so the subscription quietly uses the price's default. Checkout picks the local currency from the customer's IP address, which is a different mechanism from the customer-level default and worth not confusing with it.
Stripe's newer Adaptive Pricing for subscriptions lands in the same place. Its own documentation says the chosen currency remains locked in until the subscription is canceled, and that existing subscriptions are not affected. Each of these routes still settles a customer on one currency for as long as their subscription is live.
The decision this one gets confused with
If you are deciding how the prices themselves are presented rather than which currencies exist, an annual and monthly toggle in Next.js covers the display-versus-billing distinction.
npm i @buildbase/sdk