Overview
Add subscription plans, checkout, trials, and seat-based pricing to your app.
BuildBase handles subscription billing end-to-end. Use the headless PricingPage component for checkout, and openWorkspaceSettings('subscription') for management. Everything else - Stripe integration, plan switching, trials, invoices - is handled automatically.
Warning
Billing requires setup in the BuildBase dashboard before the SDK components will work:
- Connect Stripe - Go to Billing → Credentials and enter your Stripe API keys (use test keys for development)
- Create at least one plan - Go to Billing → Plans, create a plan with a name, price, and billing interval
- Publish a plan version - Plans use versioning. Create and publish at least one version so it appears on the pricing page
- Set up a pricing group - Go to Billing → Pricing groups. Group your plans so
PricingPageknows what to display
Without these steps, PricingPage renders empty plans and selectPlan() fails.
import { PricingPage } from '@buildbase/sdk/react';
function Pricing() {
return (
<PricingPage slug="main-pricing">
{({ loading, error, plans, selectPlan }) => {
if (loading) return <p>Loading plans...</p>;
if (error) return <p>{error}</p>;
return (
<div className="grid grid-cols-3 gap-4">
{plans.map((plan) => (
<div key={plan._id}>
<h3>{plan.name}</h3>
<button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
Choose {plan.name}
</button>
</div>
))}
</div>
);
}}
</PricingPage>
);
}import { useSaaSAuth } from '@buildbase/sdk/react';
function ManageBilling() {
const { openWorkspaceSettings } = useSaaSAuth();
return (
<button onClick={() => openWorkspaceSettings('subscription')}>
Manage Subscription
</button>
);
}Before you start
Create plans and pricing groups in the BuildBase dashboard first.
PricingPage
PricingPage is headless: it fetches plan data and handles checkout logic, and you render the UI through a render prop. Both slug (the pricing group to display) and children (the render function) are required.
The render prop receives:
plans- published plan versions with pricing, features, limits, and quotasitems- subscription items (features, limits, quotas) for building comparison tablesloading/error- fetch stateselectPlan(planVersionId, interval, currency)- opens the plan dialog for authenticated users, or triggers sign-in first (setredirectBaseUrlonPricingPagefor the unauthenticated flow)refetch()- refetch plan data
Optional props: loadingFallback (custom loading UI), errorFallback (custom error UI), and redirectBaseUrl (post-login redirect base for unauthenticated visitors).
The email on the Stripe checkout page is filled in for you and cannot be edited there. It is the workspace owner's address, so a teammate with billing rights checks out under the owner's email rather than their own. Your app never passes an email: selectPlan takes no email argument and the checkout endpoint rejects one.
openPlanPicker()
Opens the plan selection UI from anywhere in your app - perfect for "Upgrade" buttons inside feature gates or trial banners:
import { useSaaSAuth } from '@buildbase/sdk/react';
function UpgradeButton() {
const { openPlanPicker } = useSaaSAuth();
return <button onClick={() => openPlanPicker()}>Upgrade Plan</button>;
}When your plans are priced in more than one currency and the workspace has no billing currency yet, the plan picker opens on the visitor's currency: the one for the country their browser's language settings name (en-GB is GBP, a bare de is Germany, so EUR). If your plans are not priced in it, it opens on USD, and without USD on the first currency offered. A checkout link's currency and the current subscription's currency come first. Nothing leaves the browser for this: there is no IP lookup and no location prompt, and the visitor can still pick another currency. The credit packages dialog does the same. From SDK 0.0.88; earlier versions open on the first currency in alphabetical order.
openWorkspaceSettings('subscription')
Opens the full subscription management screen where users can:
- View current plan and billing interval
- Upgrade or downgrade
- Cancel or resume
- View invoices and payment history
- Access the Stripe billing portal
Changing plan
A plan change from the plan picker or useUpdateSubscription() is applied to the workspace's existing subscription. What happens next depends on the subscription:
- During a trial, the plan changes and the trial keeps running: it ends when it would have, and nothing is charged at the change. The first charge is the new plan's price, when the trial ends. A trial started without a card still needs one before then, or Stripe cancels the subscription at the end of the trial.
- An upgrade invoices the difference at once. When the card needs 3-D Secure or is declined, Stripe holds the upgrade until that invoice is paid, and the workspace stays on its current plan meanwhile. Plan & Billing shows a Complete payment button that opens Stripe's invoice page (from SDK 0.0.86; earlier versions say the plan was updated). If the invoice is not paid within 23 hours, Stripe drops the change.
- A downgrade is not credited back. The workspace keeps its current plan's features until the end of the period it paid for.
The subscription.upgraded webhook fires when the upgrade is applied: at the change, or once its payment goes through.
Calling the hook yourself, check for a payment the user still has to make:
const result = await updateSubscription(planVersionId);
if ('checkoutUrl' in result) {
window.location.href = result.checkoutUrl; // no subscription yet: a first checkout
} else if (result.paymentRequired && result.paymentUrl) {
window.open(result.paymentUrl, '_blank', 'noopener,noreferrer'); // the upgrade waits on this payment
}paymentRequired and paymentUrl are declared on ISubscriptionUpdateResponse from SDK 0.0.86; the server sends them to every version.
Reading subscription state
Use hooks when you need to display subscription info or conditionally render UI:
import {
useSubscription,
useTrialStatus,
WhenNoSubscription,
WhenSubscriptionToPlans,
WhenTrialEnding,
} from '@buildbase/sdk/react';
function Dashboard({ workspaceId }) {
const { subscription } = useSubscription(workspaceId);
const { isTrialing, daysRemaining } = useTrialStatus();
return (
<>
{subscription && <p>Plan: {subscription.plan?.name}</p>}
{isTrialing && <p>{daysRemaining} days left in trial</p>}
<WhenNoSubscription>
<p>
No plan yet. <a href="/pricing">Choose a plan</a>
</p>
</WhenNoSubscription>
<WhenSubscriptionToPlans plans={['starter']}>
<p>Upgrade to Pro for more features.</p>
</WhenSubscriptionToPlans>
<WhenTrialEnding daysThreshold={3}>
<p>Trial ends soon - add a payment method to keep access.</p>
</WhenTrialEnding>
</>
);
}Hooks
| Hook | What it returns |
|---|---|
useSubscription(workspaceId) | Current plan, status, billing interval |
useTrialStatus() | isTrialing, daysRemaining |
useSeatStatus(workspace) | memberCount, includedSeats, canInvite, inviteBlockReason |
Conditional components
| Component | Renders when |
|---|---|
WhenSubscription | Has an active subscription |
WhenNoSubscription | No subscription |
WhenSubscriptionToPlans | On a specific plan (pass plans prop) |
WhenTrialing | In trial period |
WhenNotTrialing | Not in trial |
WhenTrialEnding | Trial ends within daysThreshold days |
Server-side
Check subscription status from your backend:
import BuildBase from '@buildbase/sdk';
const bb = BuildBase({ serverUrl, orgId, getSessionId });
const { subscription, plan } = await bb.subscription.get(workspaceId);
if (!subscription || subscription.subscriptionStatus !== 'active') {
return Response.json({ error: 'Subscription required' }, { status: 403 });
}Next Steps
- Pricing Page - Order, group and highlight what each plan includes.
- Credits & Usage - Add consumption-based billing.
- Permissions - Gate billing UI by role.