Subscriptions
Redirect to Stripe Checkout, handle subscription lifecycle states, and manage trial periods.
Checkout flow
Call createCheckoutSession from @nebutra/billing to generate a Stripe-hosted checkout URL, then redirect the user to it.
import { createCheckoutSession } from "@nebutra/billing";
import { redirect } from "next/navigation";
import { getCurrentTenant } from "@nebutra/tenant";
export async function GET() {
const tenant = getCurrentTenant();
const session = await createCheckoutSession({
orgId: tenant.tenantId,
priceId: process.env.STRIPE_PRO_PRICE_ID!,
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`,
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/billing`,
trialDays: 14,
});
redirect(session.url);
}Stripe handles the payment form, SCA/3DS, and error states. You do not need to build a payment form.
On a successful payment, Stripe redirects the user to your successUrl. Show a confirmation UI — but do not update the plan here. Plan activation is confirmed by the webhook.
export default function BillingSuccessPage() {
return (
<div>
<h1>You're on PRO!</h1>
<p>
Your plan is being activated. This usually takes a few seconds.
Refresh the page if your new limits aren't reflected immediately.
</p>
</div>
);
}Stripe fires checkout.session.completed. The webhook handler in @nebutra/billing updates the tenant plan in the database. Quota limits update automatically.
Do not update the plan based on a successful redirect URL alone. A redirect can be faked. Always wait for the checkout.session.completed webhook to confirm the payment.
Checking subscription status
Use getSubscription to fetch the current subscription state for an organization:
import { getSubscription } from "@nebutra/billing";
const subscription = await getSubscription("org_123");
// → {
// orgId: "org_123",
// plan: "pro",
// status: "active",
// currentPeriodEnd: "2026-04-30T00:00:00.000Z",
// cancelAtPeriodEnd: false,
// trialEnd: null,
// }Subscription lifecycle states
| State | Meaning | User impact |
|---|---|---|
trialing | Within the 14-day trial period | Full PRO access, no charge yet |
active | Subscription is current and paid | Full plan access |
past_due | Latest invoice payment failed | Plan remains active; user is prompted to update payment |
canceled | Subscription was canceled | Downgraded to FREE at period end |
unpaid | Multiple failed attempts; Stripe paused subscription | Access revoked; user must update payment method |
Displaying status in the UI
import { getSubscription } from "@nebutra/billing";
import { getCurrentTenant } from "@nebutra/tenant";
export default async function BillingSettingsPage() {
const tenant = getCurrentTenant();
const subscription = await getSubscription(tenant.tenantId);
return (
<div>
<p>Current plan: <strong>{subscription.plan.toUpperCase()}</strong></p>
<p>Status: {subscription.status}</p>
{subscription.status === "past_due" && (
<a href="/billing/portal">Update payment method</a>
)}
</div>
);
}Trial periods
PRO subscriptions include a 14-day free trial. No credit card is charged until the trial ends.
- Trial state is reflected as
status: "trialing"ingetSubscription. - When the trial expires, Stripe charges the card on file and moves the status to
active. - If no payment method is provided before trial end, the subscription moves to
canceledand the tenant is downgraded to FREE.
// Pass trialDays when creating the checkout session to activate a trial
const session = await createCheckoutSession({
orgId: "org_123",
priceId: process.env.STRIPE_PRO_PRICE_ID!,
successUrl: "https://app.nebutra.com/billing/success",
cancelUrl: "https://app.nebutra.com/billing",
trialDays: 14,
});You can set trialDays: 0 to skip the trial for specific users (e.g., users migrating from a legacy plan or those using promo codes).
Cancellation and self-service
Customers can cancel, upgrade, or update their payment method via the Stripe Customer Portal — no custom UI required:
import { createPortalSession } from "@nebutra/billing";
import { redirect } from "next/navigation";
import { getCurrentTenant } from "@nebutra/tenant";
export async function GET() {
const tenant = getCurrentTenant();
const session = await createPortalSession({
orgId: tenant.tenantId,
returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
});
redirect(session.url);
}This route is already wired to the Settings → Billing → Manage Subscription button in the dashboard.
Related
How is this guide?
Last updated on