When you need a second pricing page, and when you need a plan version
When a second pricing group is the right call and when it is a plan version or a currency variant instead, plus what a second public pricing page costs you in attribution and in-app upgrades.
In short
Three different things get called a second pricing page. A different price is a new plan version, a different currency is a pricing variant, and only a different set of plans is a second pricing group. Split for the third: a second group costs you attribution and an upgrade path that ignores your slug.
The console calls it a pricing group. Your marketing site calls it the pricing page. People reach for a second one about a month before launch, usually to solve a problem it will not solve.
Here is the distinction nobody writes down. A pricing group is a named, slugged, versioned list of published plan versions. That is the whole object. It holds no price, no currency, no region and no customer. So when someone asks for a second pricing page, one of three different things is being asked for, and only one of them is a group.
What you are building
One decision, recorded in the console: whether your organization publishes one public pricing table or several, and which of them is the default.
When it works, a visitor at /pricing sees the group you named, and a
signed-in customer who clicks Upgrade sees plans they can actually buy. When
it does not, a customer who arrived through /pricing/startups opens the
upgrade dialog six weeks later and finds the standard table, because the
in-app endpoint behind that dialog takes no slug. Nothing errors. They just
see the wrong prices, and you hear about it from them.
Before you start
You need billing currencies set on the organization, which is step one of the Get Started checklist and worth settling before you build any plans, plus Stripe connected and at least one published plan version. A group version refuses to publish while it is empty, and it will not take a plan version that has no price in your organization's first billing currency - that second one is checked as you add plans to the draft, not at publish time.
Step 1: run the test before you create anything
The test is about the list of plans, not about the money.
The price differs, the plans do not. That is a plan version. Published plan versions are immutable, so a price change means publishing a new one, and the customers already subscribed stay on the version they bought. No second page involved.
The currency differs, the plans do not. That is a pricing variant on the plan version. Base price and quota overage are both per currency, the checkout call takes the currency as an argument, and the workspace locks to one currency the first time it subscribes.
The set of plans differs by audience. Now you want a group.
That middle case is where most "regional pricing" questions actually land, and there is a constraint that settles it. A group version refuses to publish unless every plan version in it supports the same currencies:
All plan versions in a group must support the same currencies. The
following plan version(s) have a different set of currencies: <names>.
Add or remove pricing variants so every plan version has the same
number and set of currencies.Combine that with the other rule - only the highest published version of a plan may sit in a group - and a group can never be the regional switch. The same plan cannot appear in one group at a euro-only version and in another at a dollar-only version, because only one of those versions can be its highest.
Charging genuinely different numbers by region, rather than the same number converted, means separate plans with their own Stripe prices, sitting in the one catalog you already have. Know that before you promise regional pricing to anyone.
Key takeaway
A second group is for a different set of plans, not a different price and not a different currency. If the plan list is identical on both pages, you are about to build a maintenance burden with no behaviour behind it.
Step 2: the cases that do earn a second group
Three that hold up:
A second product line with its own plans, sold from its own page. A partner
table at /pricing/partners listing a different subset of what you already
sell. And a plan you want reachable by link but absent from /pricing, since a
non-default group is never returned by the default endpoint, which makes an
unlisted table just a group you did not promote.
Everything else I have been asked for turned out to be one of the two cheaper
things above. Worth knowing before you plan around any of it: creating a group
is checked against a max-plan-groups ceiling on your own subscription, so the
number you get is not open-ended.
Step 3: pick the default on purpose
At most one group per organization is the default. The console's Set default action is a single call that promotes one and demotes the rest, so nothing lets you have two. Having none is reachable, though, and it is its own problem. More on that below.
That flag does more work than its name suggests. It decides what the public default endpoint returns for a site that renders pricing without naming a group, and it decides what a workspace with no subscription sees inside your app. Pick it rather than inheriting it from whichever group you happened to create first.
Two mechanical notes while you are there. The slug default is reserved and
the create route rejects it, since the public API already uses that path. And
a group carries one draft version at a time, so a second draft is refused
until you publish or delete the first.
The slug is the part you paste into your site:
import { PricingPage } from '@buildbase/sdk/react';
export default function Pricing() {
return (
<PricingPage slug="main" redirectBaseUrl="https://example.com/app">
{({ plans, loading, selectPlan }) => {
if (loading) return <p>Loading plans</p>;
return plans.map((plan) => (
<article key={plan._id}>
<h2>{plan.name}</h2>
<button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
Choose
</button>
</article>
));
}}
</PricingPage>
);
}slug and the render function are both required. Point a second route at a
second slug and you have two public pricing pages.
Step 4: the half that is not on your marketing site
Your signed-in app also shows plans, and it does not go through PricingPage.
It calls this:
GET /api/v1/public/workspaces/:workspaceId/subscription/plan-groupRead that path again. There is no slug in it. So which group does it return? The endpoint resolves one on its own, in this order: the group recorded on the workspace's subscription, or the organization default, or, failing both, the most recently created group version in the organization. Not the most recently published one. That last query filters on neither status nor currency, so an unfinished draft is what an app can fall back to.
So the upgrade dialog is correct for a customer who already subscribed, and correct by luck for everyone else. A trial workspace that signed up from your partner page has no subscription yet. It gets the default group. The pricing page promised one table and the app offers another, and the only symptom is a confused support email.
If you run a second group, the signed-in path is the work. Either keep the plans in both groups close enough that the fallback is harmless, or get the customer onto a subscription in the right group before they ever open the dialog.
What breaks later, and how you find out
Your conversion numbers quietly merge. A subscription does not record the pricing page the customer came from. The group is resolved afterwards by walking every live group, default first, and taking the first current published version that contains that plan version. Share one plan version between two groups and every subscription for it is attributed to the default group. No error, no warning, just a funnel report that says your second page converted nobody.
Two groups you want to tell apart need two sets of plans, which is the opposite of what the upgrade dialog wanted in step 4. Share the plan versions and the fallback is harmless but the funnel is unreadable; split them and you get the reverse. No setting gives you both.
You try to clean up and cannot. Archiving or deleting a group with live subscriptions is refused outright:
Cannot delete this pricing group - it has active subscriptions. Archive
it after all subscriptions are migrated or canceled.Migrating those customers is a per-customer plan change, not a toggle. Which is the honest summary of this whole decision: a group is cheap to create and expensive to retire, exactly like enabling a currency you later regret.
You delete the default and the pricing page empties. The delete response carries a warning when the group was the default, and the public default endpoint then answers with a plain 404 saying no default pricing group is configured. A site that renders pricing without naming a slug shows nothing at all. Set a new default in the same sitting.
You add last year's plan to a new group. Only published versions are allowed in, and only the highest published version per plan. A group is always a view of your current catalog, never an archive of it. Keeping an old price alive is a plan version job, and the two get confused often enough that it is worth saying twice.
One group is usually the answer
Our own organization sells three plans: Launch, Grow and Scale. Our pricing
page reads one group slug, main, out of a single environment variable. The
catalog in that organization ran to 163 rows the last time we read production,
and only 105 of those are shown to a customer at all. A catalog that size still
goes out on one table.
That is the thing to weigh. The cost of one group is that one page has to speak to everyone. The cost of two is a second set of plans to keep current, a split funnel, an upgrade path that needs thinking about, and a group you cannot delete once somebody buys from it.
A group's slug is friendlier than a catalog item's. The API will take a new one after a duplicate check, because nothing is keyed on it internally. It is still wired into your public route and into the variable your site reads, so renaming it is a deploy rather than an edit.
Create the second group when the set of plans genuinely differs. Until then, the answer to "we need another pricing page" is almost always a plan version.
npm i @buildbase/sdk