Skip to main content
Engineering7 min readDecember 22, 2025

Implementing Multi-Tier Stripe Billing for Routiine.io

How I built the subscription billing system for Routiine.io — Stripe integration, plan tiers, usage metering, and handling the edge cases that documentation does not cover.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

The Billing Requirements

Routiine.io needed a billing system that supported three plan tiers, per-seat pricing within each tier, annual and monthly billing cycles, a free tier with functional limitations, upgrade and downgrade flows, and prorated charges for mid-cycle plan changes. This is a common set of requirements for a SaaS product, and Stripe handles most of it natively — but the gaps between what Stripe provides and what a production billing system needs are where the real work lives.

The previous experience with Stripe in BastionGlass covered transaction-based payments. Subscription billing is a fundamentally different pattern. Transactions are discrete — each payment is independent. Subscriptions are continuous — they create ongoing relationships with recurring charges, entitlement management, and lifecycle events that span months or years.

Stripe Products and Price Architecture

We structured the Stripe product catalog around the three tiers: Starter, Professional, and Enterprise. Each tier has two prices — monthly and annual — and each price uses per-seat billing. This means the charge for a Professional monthly subscription with five users is different from one with three users, and both are different from the annual equivalent.

Stripe models this naturally with Products (the tier) and Prices (the specific billing configuration). Per-seat pricing uses Stripe's quantity parameter on the subscription — the quantity represents the number of seats, and Stripe multiplies the per-seat price by the quantity to calculate the charge.

The tricky part is seat management. When a customer adds a user to their Routiine.io account, the application needs to update the Stripe subscription quantity. When a user is removed, the quantity decreases. Each of these changes triggers prorated billing — Stripe calculates the cost of the remaining portion of the billing period with the new quantity and adjusts the next invoice accordingly.

We handle this with a synchronization function that runs whenever users are added or removed from an account. The function reads the current user count from the database, compares it to the Stripe subscription quantity, and updates Stripe if they differ. This approach is resilient to race conditions — even if two admins add users simultaneously, the sync function converges to the correct quantity because it reads the ground truth from the database rather than trying to increment or decrement.

Webhook Processing

Stripe webhooks are the backbone of subscription billing. Events like invoice.paid, invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted drive the application's understanding of each customer's billing state.

Webhook processing in Routiine.io follows a pattern I have standardized across projects. The webhook endpoint validates the Stripe signature, parses the event, and dispatches it to a type-specific handler. Each handler is idempotent — processing the same event twice produces the same result. This is essential because Stripe may deliver webhooks more than once, and the application needs to handle duplicates gracefully.

The invoice.paid handler updates the account's billing status and extends the service period. The invoice.payment_failed handler transitions the account to a grace period — features continue to work for a configurable number of days while payment is retried. The customer.subscription.deleted handler downgrades the account to the free tier, preserving data but restricting functionality.

Each handler runs within a database transaction to ensure that the billing state and the entitlement state are always consistent. If the entitlement update fails after the billing state is recorded, the transaction rolls back and the webhook is retried. This prevents the situation where a customer is charged but their features are not activated, or vice versa.

Entitlement Enforcement

Billing and entitlements are separate concerns that need to stay synchronized. The billing system manages charges and subscription lifecycle. The entitlement system manages what features and resources each account can access. They communicate through the webhook handlers, but the entitlement checks happen at the application layer.

Every API endpoint in Routiine.io passes through an entitlement middleware that checks whether the requesting account has access to the requested feature. The middleware reads from a local entitlements table — not from Stripe — which means entitlement checks add zero external latency to API requests.

The entitlements table stores the account's current tier, seat count, and feature flags. Feature flags map specific capabilities to tiers: Salesforce integration requires Enterprise, advanced analytics requires Professional or higher, basic pipeline management is available on all tiers including Free.

This separation means we can adjust the feature mapping without changing the billing configuration. If we decide to move advanced analytics from Professional to Starter, it is a configuration change in the entitlements mapping, not a Stripe product restructure. The billing and the features are decoupled, which gives us flexibility to adjust positioning without engineering work.

The Edge Cases

The documented Stripe integration flows work well for the standard cases — new subscription, upgrade, downgrade, cancellation. The edge cases are where the engineering effort concentrates.

What happens when a customer's card expires and they do not update it? Stripe retries the charge according to a configurable schedule. During the retry period, the account should continue to function — cutting off access immediately for a failed auto-renewal is a terrible customer experience. We implemented a seven-day grace period where the account retains full functionality while displaying a billing alert. After seven days without successful payment, the account downgrades to Free tier.

What happens when a customer disputes a charge? Stripe notifies us via the charge.dispute.created webhook. We do not automatically restrict the account during a dispute because the customer may be legitimate and the dispute may be resolved in our favor. But we do flag the account for review and pause any upcoming charges until the dispute is resolved.

What happens during a Stripe outage? Entitlement checks use the local database, so the application continues to function. New subscriptions cannot be created during the outage, but existing subscriptions are unaffected because their entitlements are already stored locally. When Stripe recovers, the webhook backlog processes and any missed state changes are applied.

These edge cases are not exotic scenarios — they happen to every SaaS product at scale. Handling them well is the difference between a billing system that works and one that works in production.