Most billing systems start as a cron job that reads a table and creates invoices. That works until usage enters the picture, then credits, then mid-cycle plan changes, then a webhook that silently fails on a Friday and takes two people a day to unwind.
The fix is not a better cron job. It's making billing deterministic: every state change emits an event, every event is idempotent, and every number on an invoice traces back to a stored record rather than a recomputation.
What does automating subscription billing actually mean?
It means every billing event triggers the next step without a person in the loop, across six layers that each do one job. Subscription management holds plan state and renewal schedules. Metering records consumption. Rating converts usage into money. Invoicing assembles and posts documents. Payments collect and retry. Entitlements keep product access matched to what was paid for.
Each layer runs independently and communicates through events. That separation is what lets you change pricing without touching product code, and replay a bad day without hand-editing invoices.
Here are the thirteen steps, in build order.
1. Map the workflow from signup to renewal
Before building anything, draw the path a customer takes and mark every point where money or access changes.
The events that matter: signup, trial start, plan activation, upgrade, downgrade, pause, cancel at period end, renewal at the anchor, usage thresholds crossed, credit grant, top-up, expiry, invoice finalized, payment succeeded, payment failed, refund, credit note.
Then document where each piece of data originates and where it goes. The product and data pipeline produce usage events. The billing service turns usage into rated lines. Invoicing creates posted documents. The payment provider returns success or failure. Tax and accounting receive posted invoices and settlements.
Two policy decisions save arguments later. Pick your billing anchor, calendar or anniversary. And pick a finalization window for late usage, so invoices close after a short grace period and anything later becomes an adjustment next cycle.
Come out of this step with three artifacts: a swimlane diagram of the event flow, an event catalogue with schemas and sample payloads, and a policy sheet covering anchors, proration, late usage, dunning, refunds, and credit notes.
2. Build on an event-driven backbone
Every meaningful subscription change emits a domain event that other services react to. Flexprice publishes this set for subscriptions: subscription.created, subscription.updated, subscription.paused, subscription.resumed, and subscription.cancelled, alongside invoice events such as invoice.create.drafted and invoice.update.finalized, and payment events such as payment.success and payment.failed. The full list is in the webhooks reference.
Whether you consume those or emit your own, each internal event should carry a globally unique idempotency key, event and delivery timestamps in UTC, resource identifiers, and version metadata so consumers know how to parse it. An internal envelope might look like this. It is your own schema, not a provider's payload format:
{
"event": "subscription.updated",
"event_id": "evt_01HQ8K3M",
"version": "2",
"occurred_at": "2025-10-27T08:14:02.118Z",
"data": {
"customer_id": "cust_123",
"subscription_id": "sub_998",
"change": "plan_upgrade",
"effective_at": "2025-10-27T08:14:02.118Z"
}
}
Internally, publish to a queue or stream so consumers get replay and backpressure. Use webhooks for external systems, but do not make an external webhook the only path for anything that affects revenue. A webhook that silently fails is an invoice that silently doesn't exist.
Every consumer guards against duplicates with an idempotency store keyed on event ID, and handlers are written to be safely reentrant. Store events durably so you can replay a window after an outage, backfill a downstream system, or reconstruct what happened during an incident. When schemas change, version them and support both formats through a migration window rather than breaking consumers.
3. Meter usage into an append-only ledger
Decide what a unit is: API calls, minutes, storage, messages, GPU seconds. Then define the unit name, the conversion to billing currency, rounding and precision rules, and the measurement window.
Every usage record carries a unique event identifier, customer and subscription IDs, the metric name and quantity, an event timestamp and a received timestamp, and any properties you might price or group by later.
Store it append-only. Never overwrite or delete a usage row. Corrections become compensating entries, which is what makes the ledger auditable and what lets you explain a number six months later.
Late and duplicate data are the two failure modes worth designing for explicitly. Late data is handled by the finalization window from step 1: close the invoice when the window passes and push delayed usage into the next cycle as an adjustment. Duplicates are handled by an idempotency constraint on the event ID.
Validate on the way in. Drop impossible values, flag missing identifiers, and alert on ingestion lag and abnormal spikes. Preventing a missing event is cheaper than rebuilding trust after an overcharge. Event ingestion covers the contract and the debugging path.
4. Rate usage against a versioned price book
Pricing should change without a deploy, which means rates live in data rather than constants.
A price book entry holds the metric and unit, the rate per unit or per tier, currency and region, any discounts or commitments, and a version ID with effective dates.
Rating then matches each usage record to the price book version that was in effect at the event's timestamp. Treat rating as a pure function: the same usage and the same price version always produce the same amount. That property is what prevents billing drift after a price change, and it's what makes a disputed invoice reproducible.
Hybrid pricing runs through the same path. A fixed access fee, metered usage, and credit drawdown all become rated lines, so the invoice assembly logic does not need a special case per pricing model. Apply minimums before overages and deduct credits in a defined order, recording each adjustment in the rated ledger rather than computing it inline.
Keep rated results in their own store, separate from raw usage: customer ID, metric, applied rate, price version, and subtotal. Invoices regenerate from that without touching the usage ledger.
5. Automate recurring billing and proration
Each subscription stores its anchor date, renewal type, and next period. That's the trigger for the bill run.
A scheduled bill run finalizes invoices for every subscription reaching its renewal date, pulling fixed charges, rated usage, and credits within the period. It also handles draft generation, tax and currency calculation, posting the finalized invoice, and creating the payment intent.
Mid-cycle changes need a proration policy, and there are two workable options. Immediate proration bills the adjustment right away. Deferred proration reflects it on the next invoice. Deferred tends to produce fewer refund requests on usage-priced products, because the customer sees one number instead of three. Pick one and apply it consistently. Understanding proration covers the mechanics.
Minimums and overages belong inside the bill run, comparing rated totals against the commitment before finalizing rather than correcting afterwards.
6. Add a credit system customers can follow
Decide what a credit represents and keep the definition stable across plans: money, units, or access. One credit might be one API call or one minute of processing.
Track credits like currency, in an append-only ledger. Every grant, spend, and expiry is its own entry carrying the customer and subscription ID, the credit type, quantity, remaining balance, and a reference to the usage event or invoice that caused it.
Automate the lifecycle: top up when the balance drops below a threshold, expire unused credits on a defined schedule, and notify before expiry rather than after. When an invoice is assembled, credits deduct before usage and recurring fees are charged.
Then show the balance. Customers who can see what they have left open fewer tickets and get fewer surprises. Auto top-up and the wallet balance types cover the configuration side.
7. Generate invoices from the rated ledger
Invoices read from the rated ledger. They do not run live queries and they do not recalculate, because a recalculation at invoice time is a second implementation of your pricing logic that will eventually disagree with the first.
For each cycle, assembly fetches fixed charges and add-ons, aggregates rated usage lines in the window, applies credits, discounts, and minimums, then adds tax and totals. Grouping lines by category keeps the document readable during an audit or a customer review.
Once posted, an invoice is immutable. Corrections appear as credit notes or adjustment lines in the next cycle. Editing a posted invoice is how phantom balances appear in accounting.
Store enough metadata to reconcile later: invoice and customer IDs, period start and end, currency, tax region and exchange rate, payment status and reference, and the price book version used. Invoices covers generation, status handling, and payment tracking.
8. Collect payments with a retry ladder
A finalized invoice creates a payment intent. The system attempts the charge, records success or failure as events, and updates the invoice and customer balance.
Payment failures are not an edge case, so the retry schedule is part of the design. A common ladder retries after 24 hours, again after 72 hours, and a final attempt at day 7, with access paused or downgraded if it's still unpaid.
Pair each retry with a message. First failure is a reminder with the retry schedule. Second includes a link to update the payment method. Final attempt warns about the access change. When a retry succeeds, restore access, mark the invoice paid, and confirm, all automatically, so there's no window where a customer has paid and is still blocked.
Store every payment attempt, gateway reference, and status against the invoice ID. Manual reconciliation stops being feasible somewhere in the low hundreds of accounts.
9. Sync entitlements the moment money or plan state changes
Entitlements are what the customer can use: features, usage caps, and credit-backed access. They have to move the instant billing state moves, or product access and billing data drift apart.
Keep entitlements in their own layer rather than inside billing or product code. That layer subscribes to billing events, maintains the current state per customer, and exposes a fast check the product calls in the request path.
The transitions to automate: payment succeeded activates or extends access, payment failed restricts or pauses, upgrade expands immediately, downgrade or cancellation reduces or revokes. Keep a history of every entitlement change with the triggering event, because that's what you'll need when someone asks why a customer lost access on a Tuesday.
10. Automate lifecycle communication
Billing that works silently still generates support tickets if customers can't see what's happening.
Trigger messages on subscription start and renewal, upcoming renewal, invoice issued, payment receipt, payment failure and retry, low or expiring credit balance, plan change confirmation, and trial expiry.
Send through the channel that fits: email for invoices and receipts, in-app for credit and usage warnings, webhooks for enterprise accounts that want to ingest billing events into their own systems. Keep templates versioned and log delivery status so you can answer what a customer actually received. Webhooks covers the outbound event contract.
11. Close the loop with tax, accounting, and audit
Automate tax calculation so every invoice carries the jurisdiction and rate, the customer's tax ID and exemption status, and the evidence used to determine location.
Push every posted invoice and payment to accounting with totals, taxes, status, and recognition schedule. Recognize revenue progressively against the billing period to stay aligned with ASC 606 and IFRS 15.
Keep immutable logs for every invoice, refund, and adjustment, with timestamps and actor. Reconcile against payment providers on a schedule, matching by invoice ID and posting adjustments for mismatches.
12. Monitor and rehearse failure
Automation without observability is a system that fails quietly.
Track event ingestion lag, rating latency, invoice finalization time, payment success and failure rates, and dunning recovery. Alert on every job failure, retry with exponential backoff, and keep dead-letter queues so failed events can be replayed rather than lost.
Then rehearse. Disable a webhook on purpose, inject a duplicate event, and confirm that idempotency and reconciliation behave the way the design says they do. A billing system that has never been tested under failure is a billing system whose failure behaviour is unknown.
13. Run a shadow bill before going live
Generate invoices as if billing were live, without charging anyone. Compare totals, credits, and taxes against what you expect.
Mismatched totals, missing usage, and double-rated events all show up here. Fix them in metering or rating, not by patching invoice data, because patched data means the bug is still there for next cycle.
Run at least one full cycle in shadow mode, confirm the finalization window behaves correctly, and document the flow for the teams who will operate it.
Where Flexprice fits
Building all thirteen steps yourself is a platform project, and the failures usually come from coordination between layers rather than from any single layer's logic. One missing webhook, one duplicate usage event, or one late credit update breaks the chain.
Flexprice is enterprise-grade, open source usage based billing infrastructure for AI and SaaS companies. It can be deployed in your own VPC, on-prem, or on Flexprice's managed cloud. Flexprice offers three deployment options, and all three run the same engine.
Against the steps above, it provides real-time event ingestion with deduplication, an append-only usage ledger, rating against versioned prices, recurring billing with proration, credit wallets with grants and expiry, invoices generated from rated records, payment retries, and entitlement state the product can query directly. Because it's open source and self-hostable, usage and revenue data can stay entirely inside your own infrastructure and never reach a vendor's cloud.
Clone it and run a shadow cycle against your own event stream: github.com/flexprice/flexprice.
Top comments (0)