SaaS PlatformBy Abdul Rahman13 May 20263 min read
SaaS Subscription Billing: Plans, Free Trials and Webhooks Done Right
Billing is where SaaS products quietly lose revenue. How to model plans and entitlements, run free trials, and keep your database in sync with the payment provider using webhooks.

Billing looks like a solved problem — plug in a payment provider and you're done. In practice it's one of the most error-prone parts of a SaaS product. Missed webhooks, trials that never convert, and plans hard-coded into business logic all cost real revenue.
This guide covers the billing architecture I recommend for subscription SaaS: how to model plans, run trials, and keep your system consistent with the payment provider.
The payment provider is the source of truth for payments; webhooks keep your entitlements in sync.
Separate Plans from Entitlements
The most common mistake is checking the plan name inside business logic:
if (tenant.plan === "pro") { /* allow exports */ }
Every pricing change then becomes a code change. Instead, map plans to entitlements — the features and limits a tenant has — and check entitlements everywhere:
const PLANS = {
starter: { maxStudents: 200, exports: false, mobileApps: true },
growth: { maxStudents: 1000, exports: true, mobileApps: true },
} as const;
function can(tenant: Tenant, feature: keyof Entitlements) {
return Boolean(tenant.entitlements[feature]);
}
Now marketing can rename, repackage or discount plans without touching feature code, and you can grant individual customers custom limits.
Free Trials That Convert
Free trials are one of the most effective acquisition tools in B2B SaaS — Nuxused, for example, offers schools a 30-day free trial. A trial only works if users reach value quickly, so the engineering priorities are:
- Frictionless start. No card required for the trial lowers the barrier significantly.
- Guided onboarding. Seed sensible defaults so the account isn't an empty database.
- Clear state. Store
trial_ends_atand show remaining days in the product. - Graceful expiry. When the trial ends, move to a read-only or limited state rather than deleting data. Users who come back later should find their work intact.
Webhooks Are the Source of Truth
Never grant access based on a redirect back from the checkout page — users can close the tab, and redirects can be forged. The payment provider tells you what happened through webhooks.
A robust webhook handler has four properties:
- Verified — check the signature using the provider's signing secret before trusting the payload.
- Idempotent — providers retry deliveries; store processed event IDs and ignore duplicates.
- Fast — acknowledge quickly and move slow work to a background job.
- Order-tolerant — events can arrive out of order; fetch the current subscription state instead of applying deltas blindly.
export async function POST(req: Request) {
const event = verifySignature(await req.text(), req.headers.get("signature"));
if (await db.processedEvent.exists(event.id)) return new Response(null, { status: 200 });
await queue.add("sync-subscription", { customerId: event.data.customer });
await db.processedEvent.create(event.id);
return new Response(null, { status: 200 });
}
Handle the Unhappy Paths
Revenue leaks through edge cases:
- Failed renewals — use the provider's retry schedule and notify the customer before access changes.
- Downgrades — decide what happens when usage exceeds the new plan's limits.
- Cancellations — typically keep access until the end of the paid period.
- Taxes and invoices — B2B customers expect proper invoices; use the provider's tax and invoicing features rather than building your own.
Key Takeaways
- Model plans as entitlements; never check plan names in business logic.
- Make trials frictionless, guided and non-destructive when they expire.
- Treat verified, idempotent webhooks as the source of truth for access.
- Design for failed payments, downgrades and cancellations from the start.


