Home / Projects / Billing API

Billing API

A single billing service backing the whole portfolio. Stripe, subscriptions, metering, refunds, dunning, lifecycle email through Brevo — every new product gets payments wired in on the first day, not the third sprint.

Category  Infrastructure · SaaSStack  Node · Fastify · PostgresIntegrations  Stripe · BrevoStatus  Live

The problem

Every new product needed billing, and every new product was implementing billing from scratch. Webhook handling, trial logic, plan upgrades, proration, dunning emails — the same code, slightly different bugs, in twenty places. The cost wasn’t the first build; it was the maintenance tax across the portfolio.

What we built

Billing API is a Fastify service backed by Postgres that owns customers, plans, subscriptions, invoices, and webhook reconciliation. Every product calls a small set of endpoints — create checkout, get entitlements, cancel subscription — and never touches Stripe directly. The service handles retries, idempotency, plan changes, and downstream lifecycle email through Brevo. Jest tests cover the state machine.

The automation angle

The flagship automation is the dunning flow: failed payment triggers a sequence of escalating reminders with grace periods, and reaches a hard cancel only after the configured policy is exhausted. Win-back happens automatically when a cancelled customer renews. The sequence is product-specific without each product having to understand it — the policy lives once, in the API.

How it’s used

  • Every paid product in the TelaLoom portfolio.
  • Internal admin tools for support, refunds, and entitlement overrides.
  • Reporting rolling up MRR, churn, LTV across the whole portfolio in one place.

What it taught us

That billing is a state machine, and pretending otherwise costs you sleep. Modeling the lifecycle explicitly — with named states, named transitions, and a strict rule that webhook events can only mutate state through known transitions — turned a class of intermittent bugs into impossibilities. We carry that pattern into every client engagement that touches money.

Why a shared service instead of a shared library

The obvious alternative was an npm package every product imports. We didn’t do that, because a shared library still leaves each product owning its own Stripe webhook endpoint, its own retry logic, and its own copy of whatever bug got fixed last. A shared service means the webhook endpoint exists exactly once. When Stripe changes a payload shape or we find an edge case in proration, the fix ships once and every product is patched the moment it deploys — nobody has to bump a dependency version across twenty repos to get it.

The tradeoff is a single point of failure across the portfolio, which is why idempotency isn’t optional: every webhook handler is written to safely reprocess an event it’s already seen, because Stripe retries on anything short of a clean 200, and a service this central will eventually see a duplicate delivery. Getting that right once, centrally, is considerably cheaper than getting it right twenty times.

Want to build something like this?

We scope projects in 48 hours and ship a first demo in 1–2 weeks. Tell us what you need.

Start a project → See how we work