Billing in a B2B SaaS application rarely breaks because of a bad price list. It breaks because the billing model was built after the rest of the system and bolted onto an existing data structure a few weeks before sales started. We design web applications and B2B systems since 2006, and we see this scenario regularly. The product works, users log in, and nobody on the team can say what a single billing unit actually is. That is when the expensive part begins, because every answer requires changes to the database schema and to all the places where sales logic has already spread across the controllers.
Table of contents
A subscription model starts with the billing unit, not the price list
The first question is not “how much does the Pro plan cost”. It is “what exactly is the customer buying”. In B2B there are several answers, and at the implementation level they rule each other out. A customer can pay per seat, per company account regardless of headcount, per volume of operations, per active integration, or for a combination of these dimensions. Each option means a different data model, a different place where things are counted, and a different moment when the system has to react.
The per-seat model looks trivial in code. Until it doesn’t. Because the next question is what an occupied seat actually is. An invited user who never activated the account? An account blocked halfway through the billing period? Someone with a read-only role? Usage-based billing shifts the weight to the measurement layer: you need events, aggregation and certainty that nothing gets counted twice. A hybrid model combines both problems and adds a third one, the relationship between the base package and the overage.
A typical mistake? It goes like this. Sales sets the prices by looking at competitors, marketing publishes a comparison table, and the tech team gets the task to “implement the plans” in a system that has no concept of an organization, only user accounts. And it turns out nothing can be calculated per company, because the data hangs on individual people. This is not a pricing problem. It is an architecture problem.
In B2B, one organization usually means many users, several roles and often branches that want separate invoices. The structure should allow for this from the start, even if your first customers have two people each.
Limits and pricing plans in the data layer: entities you cannot add later without a migration
The minimum set of entities that survives several years of development looks like this:
- Organization - the data owner and the party on the invoice, separate from user accounts.
- Subscription - the link between an organization and a plan for a specific period, with a change history.
- Plan - a versioned definition of the offer, not an editable row overwritten whenever prices change.
- Plan entitlements - a list of features and limits described as data, not code.
- Usage counters - the current state per organization and billing period.
Plan versioning often gets skipped, and in my view it is the most common cause of emergency migrations later on. A customer who signed up on last year’s price list has to keep working after the offer changes. Without manual digging in the database and without an exceptions table in a spreadsheet (I have seen such spreadsheets, I don’t recommend them). Instead of modifying the plan definition, you create a new version, and existing subscriptions stay pinned to the previous one. Grandfathering then stops being a problem. It becomes a natural consequence of the model.
Sooner or later an enterprise customer will negotiate something outside the standard: a higher limit, an extra integration, longer retention. Keep such terms as a separate layer of overrides attached to the subscription, and don’t write them into the global plan. Otherwise, a year later nobody knows whether a given value is the standard offer or a leftover from one negotiation ten months ago.
Tip: keep limit definitions in data, not in if statements scattered across the application. A limit check should be a single call that queries the organization’s entitlements, not a series of plan name comparisons. Code like “if the plan is Pro or Enterprise” goes stale with the first new package and quietly fails with every one after that.
Moving customers between plans also deserves a planned procedure with a preview of the effects. Not a script written the night before a new offer goes live.
Enforcing limits: hard, soft, and the ones that break the customer’s rollout
There are three ways to handle an exceeded limit, and each has different business consequences. A hard block stops the operation and protects infrastructure costs, but it can halt a process on the customer’s side at the worst possible moment. A soft warning lets the action through and notifies the administrator - it works for limits that don’t translate directly into cost. Paid overage requires precise measurement and clear communication, because an invoice higher than expected is a complaint waiting to happen.
More important than the reaction mode, though, is where the limit is checked. Checking only in the user interface means the limit does not exist - all it takes is calling the API directly. The check has to work in the API layer, in the queue of asynchronously processed jobs and in scheduled tasks. A background import, a report generated at dawn, an integration polling the system every minute: all these paths bypass the screen with the nice message.
With parallel requests you get a classic race condition. Two processes read the same counter value, both see free capacity, both write the value plus one, and the limit is exceeded without a trace in the logs. The solution is atomic operations on the counter or batch aggregation with an accepted margin - chosen deliberately, not by accident.
Also separate two independent mechanisms. Commercial limits come from the contract and the pricing plan. Technical limits - rate limiting, maximum file size, the number of parallel connections - protect the infrastructure and apply regardless of how much the customer pays. Mixing them in one module leads to a situation where upgrading a package accidentally opens the door to overloading the system.
Risk: a hard block in the middle of a customer’s business process is one of the fastest routes to contract termination. A grace period and advance notice cost little and save the relationship.
Usage measurement: metering that holds up when an invoice is disputed
Record usage events as immutable and always with an idempotency key. A request repeated after a timeout, a retry in the queue, a redelivered webhook - each of these happens during normal system operation, and none of them may increase the charge. The idempotency key has to come from the business operation. A key generated randomly at write time protects against nothing.
Raw events are aggregated into billing periods, and a closed period no longer changes. Sounds obvious, until a late event from last month arrives. The decision whether it goes into a correction or into the current period has to be made at the design stage. Not during a call with a customer who is disputing the amount.
A separate set of traps concerns time. The organization’s time zone and the server’s time zone can differ, and the day boundary moves a charge into a different period. A plan change in the middle of the month requires proration and an unambiguous rule: does the unused part of the old package carry over to the new one, or is it lost? A downgrade during the period is a separate story, often solved by moving the effect to the renewal date.
Sooner or later a B2B customer will ask where the amount came from. And then what? The answer “that’s what the system calculated” ends trust. You need a usage report broken down to the level of individual operations, with the date, context and an export option. The same tool speeds up support, because the support team can check the data without involving a developer.
Count your own costs too. At high volume, writing every event to the same database that serves the application becomes a real load. A separate data store for metering events, with a different indexing and retention strategy, is often simply cheaper than scaling the main database.
Integration with payments and accounting: where the source of truth is
The payment provider handles transactions, cards and part of the subscription logic, but the state of a customer’s entitlements should come from your database. An application that asks an external API whether a user has access to a feature stops working whenever the provider has an outage, and it ties your data model to someone else’s structure. The external system is the source of truth about money. Yours is the source of truth about entitlements.
Webhooks arrive late, out of order and more than once. That is not a failure, just the normal nature of distributed communication. Handling has to be idempotent, resistant to events older than the current state, and backed by periodic reconciliation that compares the local state with the state at the provider. Without that second mechanism, sooner or later a customer shows up who paid but didn’t get access. Been there.
B2B specifics go far beyond the payment card. Traditional bank transfers, invoices with deferred payment terms, annual orders paid upfront, invoice details with a NIP tax number, counterparty verification. Selling abroad adds VAT OSS and the reverse charge, which changes the rate depending on the buyer’s status. These rules belong in the invoicing module, not in the controller handling the order form.
A failed payment is a process, not an error. A sensible sequence looks like this:
- An automatic retry after a few days, with a notification to the organization’s administrator.
- A grace period with full access and a clear message in the application.
- Restricting write functions while keeping read access and data export.
- Suspending the account while keeping the data for a set period.
Keep the integration with the invoicing system and the accounting export as a separate, testable module with a clear contract. That way, changing your accounting firm or the export format doesn’t touch the subscription logic.
Security, data isolation and audit in multi-company billing
Data separation between organizations has to be enforced at the query level, not by hiding elements in the interface. The organization ID should go into the filter condition in the data access layer, ideally automatically, so that a single forgotten query cannot expose someone else’s invoices. Tests should include an attempt to read another company’s resource by swapping the ID in the URL. The simplest scenario in the world and the most often neglected.
Permissions for billing data are rarely uniform. The company administrator sees invoices and can change the plan. A technical person needs usage statistics and API keys, but not necessarily the amounts. The purchasing department wants documents and payment history without access to the product configuration. A single “admin” level for all these roles works exactly until your first bigger customer.
An event log in the billing module is not a luxury. Who changed the plan, who raised the limit, when and on what basis - this information settles disputes with customers and keeps the support team’s work in order. Entries should be immutable and contain both the state before and after the change.
Machine access is a separate category. API keys and integration tokens also consume limits, so they have to be assigned to the organization, have their own restrictions and be revocable without disabling the whole account. A key created by a former employee and still working months after they left is a real security problem. Auditors ask about it almost every time.
Tip: when selling to larger companies, prepare answers in advance about billing data retention and the option to export it. These questions come up in security questionnaires and can delay signing a contract by weeks if the team still has to implement them.
Maintenance, price changes and scaling the model over time
An admin panel for handling exceptions pays for itself faster than you’d think. Extending a trial, correcting a counter after an incident, a custom limit for one customer, manually unblocking an account after a payment is cleared - everyday life in every SaaS. Without a tool, each of these is a ticket for a developer and an SQL query on production. That means risk and cost where a form with an entry in the event log would be perfectly enough.
Cover edge cases with tests, because they are the ones that generate the most complaints. A plan change exactly on the renewal date, a refund, cancellation at the end of the period with access kept until then, reactivation after a break, an upgrade with unsettled overage. Each of these touches several entities at once, and it is easy to end up in an inconsistent state that nobody notices for a month.
It is worth connecting the payment provider’s test environment from day one, together with a dataset that reproduces typical situations: a declined card, a payment requiring additional authentication, a partial refund. Testing billing changes on production ends with invoice corrections that nobody likes issuing.
Your own implementation or an off-the-shelf billing system? The complexity of the pricing and the cost of maintenance decide. A simple per-seat model with a few plans fits in your own code without effort. Billing multi-dimensional usage, multiple currencies, corrective invoices and framework agreements is an area where a specialized tool usually wins. Calculate both options instead of deciding on reflex.
Finally, observability: alerts about a mismatch between measured usage and charges should fire before the customer reports it.
Summary: billing is designed together with the product
The order of decisions is fairly fixed, and it is worth sticking to it. First the billing unit, meaning a clear answer to what the customer pays for. Then the data model with the organization, a versioned plan and entitlements described as data. Next, limit enforcement in all execution layers, not just the interface. After that, metering that is resistant to duplicates and ready to hold up in a dispute. Last, integrations with payments and accounting, plus an exceptions panel, without which the maintenance team works by hand on the database.
The most expensive fixes in this area are the ones that require migrating the billing history of a live customer. Changing the definition of the billing unit after two years means recalculating events, reconciling the state with the payment provider, correcting documents and talking to customers about amounts that suddenly look different. The cost of such a project can be comparable to building a new module, and the risk is considerably higher, because you are working on financial data in motion.
Well-designed billing gives you something that only shows over time: the ability to change prices without rewriting the application. A new plan is a row in the data, a promotion is an override on a subscription, and a pricing experiment doesn’t require deploying a new version of the system. That is an operational advantage, not a technical curiosity.
If you are planning a B2B SaaS application, rebuilding the subscription model in a live system, or integrating billing with payments and accounting - get in touch. We will design an MVP, extend an existing B2B system, connect API integrations, automate repetitive processes or modernize what already works but no longer keeps up with your offer. If AI-based solutions are also on the table, we will plan them together with the rest of the architecture, not next to it.








