Revenue Assurance for Subscriptions: My Billing Reconciliation Experiment

Most of us know what an AI subscription bill looks like. You pay a flat monthly fee for a chatbot plan, much like ChatGPT Plus. If you build an app on top of the model, you also pay for what your app uses through the API.

That bill looks simple. Behind it, several systems have to agree. One knows who is subscribed. One counts the API calls. One produces the invoice. One records the payment. When they disagree, money leaks quietly. Some customers are never billed for what they used. Others are billed twice. Nobody notices, because every bill still looks like a normal bill.

Catching that leak is called revenue assurance. In one past role, I reconciled three systems against each other: the product platforms that showed which customers were active, Salesforce for what they had ordered, and SAP for what we billed. The rule was simple. Only active customers get billed, and they get billed for what they ordered. That work found revenue we were missing, and it stopped overbilling before the bills went out.

I wanted to see how far the same idea gets you with a small, open example. So I built a made-up AI company, wrote 19 billing scenarios and ran two kinds of checks against them. A line-by-line revenue assurance check caught 12 of 14 billing errors. A quick check of the total bill caught only seven. Neither check raised a false alarm on the five correct cases.

The two errors that got through are the most interesting part. In one, a batch of API calls disappeared from every system. Every system agreed on the smaller bill, so nothing looked wrong.

What is revenue assurance?

Revenue assurance checks that a business bills every charge it is owed, bills each customer the right amount and collects what it billed. For a subscription business, that means comparing who is subscribed, what they used, what they were charged and what they paid. Missing charges, overcharges and unpaid bills each need different fixes.

Picture a customer on a $20 monthly plan who also made $10 worth of API calls. A $20 bill looks perfectly reasonable. It has the right customer, the right date and a sensible amount. It is still missing a third of what the customer owes.

Now picture a correct $30 bill that nobody paid. Billing did its job. The problem is in collections. Calling both cases “lost revenue” hides which team needs to act.

PwC’s revenue assurance paper covers leaks across contracts, service delivery, invoicing and collections. I kept this experiment narrower: monthly plans, API usage and payments. It does not cover accounting rules for when revenue is recognized.

Why test billing after testing the data?

Data quality checks confirm that each record follows the rules, such as a valid customer ID and a positive number of API calls. They cannot see a record that never arrived, or a valid record charged to the wrong customer. Revenue assurance compares the bill with an independent view of what the bill should have been.

This follows my data contract validation experiment. Those rules let three errors through: missing usage, usage charged to the wrong customer and a quantity recorded in the wrong unit. All three appear here as scenarios, and both checks in this experiment caught all three. They could, because the product usage log still had the correct API calls to compare against.

There is also a people question. In the business scenario in AI to ROI for Business Leaders, the business leader gives data quality a named owner who can pause work when the inputs are wrong. A billing error report needs the same thing. If nobody owns the fix, the report just sits there.

What does the made-up AI company sell?

The company sells two plans. Each has a monthly fee and a price for every API call. An API call is one request from a customer’s app to the AI model, such as asking it to answer a question or summarize a document. The prices are made up, and there are no real customers or payments.

Real AI APIs usually charge by tokens, the chunks of text the model reads and writes. A flat price per API call keeps the math simple enough to check by hand, and the billing problems are the same either way.

Plan Monthly fee Price per API call
Basic $20.00 $0.01
Pro $50.00 $0.02

The bills cover September 2026, and I reviewed them on October 8, 2026. There are five customers:

  • S001 and S005 are on Basic and active all month.
  • S002 is on Pro and active all month.
  • S003 canceled before September, so it owes nothing.
  • S004 is on a free trial for all of September, so it also owes nothing.

The subscription list marks each customer as billable or not, and the code trusts that flag. If the flag is wrong, the checks inherit the mistake. In a real business, someone in finance should confirm those rules.

There are no discounts, taxes, refunds, annual plans or partial months. I kept the rules simple so anyone can check the math by hand.

Where does the billing data come from?

Each scenario has six small CSV files. Amounts are stored in cents, so $30.00 appears as 3000.

File One row means Key fields Connects to
subscriptions.csv One customer’s subscription for September subscription_id, customer_id, plan_id, status, billable Plans, usage and invoices
plans.csv One plan and its prices plan_id, monthly_cents, unit_cents (price per API call) Subscriptions, by plan_id
source_usage.csv One batch of API calls, as the product logged it event_id, subscription_id, quantity (number of API calls) The billing usage log, by event_id
billing_usage.csv The same batch, as the billing system received it event_id, subscription_id, quantity The product usage log, by event_id
invoices.csv One bill invoice_id, subscription_id, customer_id, amount_cents, status, due_date Subscriptions and payments
payments.csv One payment attempt against a bill payment_id, invoice_id, amount_cents, status Invoices, and one bill can have several payments

Here is the clean case, where everything is correct.

Subscriptions

subscription_id,customer_id,plan_id,status,billable,period
S001,C001,basic,active,1,2026-09
S002,C002,pro,active,1,2026-09
S003,C003,basic,canceled_before_period,0,2026-09
S004,C004,basic,trial_full_period,0,2026-09
S005,C005,basic,active,1,2026-09

Plans

plan_id,monthly_cents,unit_cents
basic,2000,1
pro,5000,2

Product usage log

event_id,subscription_id,quantity,period
E001,S001,1000,2026-09
E002,S002,500,2026-09
E003,S002,1500,2026-09
E004,S005,500,2026-09

Billing usage log

event_id,subscription_id,quantity,period
E001,S001,1000,2026-09
E002,S002,500,2026-09
E003,S002,1500,2026-09
E004,S005,500,2026-09

In the clean case, the two usage logs match. Each error scenario changes one or both of them, or the bills and payments that follow.

Invoices

invoice_id,subscription_id,customer_id,amount_cents,status,due_date,period
I001,S001,C001,3000,paid,2026-10-05,2026-09
I002,S002,C002,9000,paid,2026-10-05,2026-09
I005,S005,C005,2500,paid,2026-10-05,2026-09

Payments

payment_id,invoice_id,amount_cents,status
P001,I001,3000,settled
P002,I002,9000,settled
P005,I005,2500,settled

“Settled” means the money arrived. Declined and pending payments do not count toward what was paid.

Download and run the experiment

The package has the data, the code that builds every scenario, the SQL, the checks, the tests and my recorded results. It runs on Python 3.10 or later with no extra installs, accounts or paid services. Enter your email below and I’ll send you the download link.

Get the code, data and results

Enter your email and I’ll send you the download link for the ZIP.





You’ll get one email from Shyam Uthaman with a download link. See the privacy policy.

Check your inbox

The download link is on its way. It works for 14 days. If it doesn’t arrive in a few minutes, check your spam folder.

Extract the ZIP, open a terminal in the revenue-assurance-experiment folder and run:

python3 experiment.py
python3 verify_experiment.py

On Windows, use py if python3 does not work. The first command runs the checks and saves the results. The second confirms the results match what I recorded. build_fixtures.py shows exactly what I changed in each scenario.

How much should each customer pay?

Each billable customer owes the monthly fee plus their API calls times the plan’s price per call. In the clean case, S001 owes $30, S002 owes $90 and S005 owes $25, for a total of $145. The canceled customer and the trial customer owe nothing.

The expected bill comes from this SQL:

WITH usage_totals AS (
    SELECT subscription_id, SUM(quantity) AS quantity
    FROM source_usage
    GROUP BY subscription_id
)
SELECT s.subscription_id, s.customer_id,
       CASE WHEN s.billable = 1
            THEN p.monthly_cents + COALESCE(u.quantity, 0) * p.unit_cents
            ELSE 0 END AS expected_cents
FROM subscriptions s
JOIN plans p ON s.plan_id = p.plan_id
LEFT JOIN usage_totals u ON s.subscription_id = u.subscription_id
ORDER BY s.subscription_id;

The query adds up each customer’s API calls before joining to the plan. S002 has two batches of API calls. Joining first would add the $50 monthly fee twice.

Customer Calculation Expected bill
S001 (Basic) $20 + 1,000 API calls × $0.01 $30.00
S002 (Pro) $50 + 2,000 API calls × $0.02 $90.00
S003 Canceled before September $0.00
S004 Free trial all month $0.00
S005 (Basic) $20 + 500 API calls × $0.01 $25.00
Total $145.00

I also kept a separate answer key with the correct amount and customer for each subscription. The checks never see it. I only use it afterward to score how much each error cost.

How did I test the billing checks?

I ran all 19 scenarios through two checks. Five scenarios are correct, including ones that look odd but are fine, like a customer paying in two parts. Fourteen contain a deliberate billing error. Each scenario starts from the clean case, so errors never pile up.

Check What it compares
Quick total check The total expected for all customers against the total of all bills
Line-by-line check Each customer’s bill, each batch of API calls, who each bill went to, duplicate bills, payments received, overdue bills and bills marked paid with no payment

Both checks use the same expected bill. Only the level of detail differs.

Revenue assurance checks for a made-up AI company: usage, billing and payment checks compare what should be billed with what was billed, catching 12 of 14 billing errors
Each line-by-line check compares what should have been billed with what was billed. The red notes mark the two mistakes every system shared.

The usage check in experiment.py is short enough to read:

source = {r['event_id']: (r['subscription_id'], int(r['quantity']))
          for r in data['source_usage']}
billing = {r['event_id']: (r['subscription_id'], int(r['quantity']))
           for r in data['billing_usage']}
for eid in sorted(set(source) | set(billing)):
    if source.get(eid) != billing.get(eid):
        flag('usage_event_mismatch', eid,
             f'Source {source.get(eid)}; billing {billing.get(eid)}.')

For every batch of API calls in either log, it checks that both logs have it, with the same customer and the same count. Before that, the code rejects duplicate IDs and references to customers that do not exist, so a bad row cannot slip through a join unnoticed.

Which billing errors did revenue assurance catch?

The line-by-line check caught 12 of the 14 billing errors. The quick total check caught seven. The line-by-line check found missing charges, overcharges, unpaid bills, a bill marked paid with no payment and a bill sent to the wrong customer. Neither check flagged the five correct cases. These counts describe the scenarios I chose, not real-world detection rates.

Scenario Quick total check Line-by-line check Underbilled Overbilled Unpaid and overdue Sent to wrong customer
Everything correct Pass Pass $0.00 $0.00 $0.00 $0.00
Customer pays in two parts Pass Pass $0.00 $0.00 $0.00 $0.00
Invoice not due yet Pass Pass $0.00 $0.00 $0.00 $0.00
Card declined, then paid Pass Pass $0.00 $0.00 $0.00 $0.00
Invoice canceled and reissued Pass Pass $0.00 $0.00 $0.00 $0.00
Active customer never billed Flagged Flagged $30.00 $0.00 $0.00 $0.00
API calls lost before billing Flagged Flagged $10.00 $0.00 $0.00 $0.00
Wrong monthly price on the bill Flagged Flagged $10.00 $0.00 $0.00 $0.00
Canceled customer still billed Flagged Flagged $0.00 $20.00 $0.00 $0.00
Customer billed twice Flagged Flagged $0.00 $30.00 $0.00 $0.00
Correct bill, never paid Pass Flagged $0.00 $0.00 $90.00 $0.00
API calls charged to the wrong customer Flagged Flagged $10.00 $5.00 $0.00 $0.00
500 API calls recorded as 5 Flagged Flagged $9.90 $0.00 $0.00 $0.00
Bill only partly paid Pass Flagged $0.00 $0.00 $60.00 $0.00
Marked paid, but no payment found Pass Flagged $0.00 $0.00 $90.00 $0.00
Two errors cancel out in the total Pass Flagged $10.00 $10.00 $0.00 $0.00
Right bill, wrong customer Pass Flagged $0.00 $0.00 $0.00 $90.00
API calls missing from every system Pass Pass $10.00 $0.00 $0.00 $0.00
Wrong price in every system Pass Pass $10.00 $0.00 $0.00 $0.00

“Flagged” means the check raised at least one issue for a person to review. The code never changes a bill or contacts a customer.

Each row is a separate what-if. Adding the dollar amounts down a column would not give a meaningful total.

Two errors can hide inside a correct total

In one scenario, S001 is overcharged $10 and S002 is undercharged $10. The total is still $145, so the quick check passes. The line-by-line check flags both customers.

This is why I would never put a single net variance at the top of a billing report. Zero can mean every bill is right. It can also mean two customers have opposite problems.

Right bill, wrong customer

Here, the correct $90 bill for S002 goes to customer C001. The amount is right and the subscription is right. The total passes, and so does the amount check. Only the check on who received the bill catches it.

No revenue is missing on paper. In practice, one customer has another customer’s bill, and the customer who owes the money may never see it.

A correct bill can still go unpaid

In the unpaid scenario, S002’s $90 bill is right, but no payment arrived by the due date. The quick check passes because billing did nothing wrong. The line-by-line check reports $90 overdue. In the partly paid scenario, $30 arrives and $60 is overdue.

Normal payment behavior should not raise alarms

Three correct scenarios test this. One customer pays $90 in two parts, $30 and $60. Another has a declined card followed by a successful payment. A third has a bill that is not due yet. None of them should be flagged, and none were.

Stripe’s invoice documentation explains that a bill can be marked paid through account credits or a payment made outside Stripe. My example does not model those, so a bill marked paid with no payment needs a person to investigate before anyone assumes the customer did not pay.

Why did two billing errors get through?

Two errors got through because every system made the same mistake. In one, a batch of API calls was missing from both usage logs. In the other, the wrong monthly price was used by both the expected bill and the real bill. Each cost $10. The checks saw systems that agreed, with nothing independent to show they were all wrong.

For the missing usage, I removed 500 of S002’s API calls from both logs. S002 is billed for 1,500 calls instead of 2,000, so the bill drops from $90 to $80. The usage logs match, the expected bill is also $80 and the customer pays $80. Everything agrees.

For the price, I changed the Pro monthly fee from $50 to $40 in the price list. The expected bill and the real bill both use $40. The bill again drops to $80, and again everything agrees.

I built these two failures in on purpose. A test with only easy wins would make the checks look better than they are. In a real business, the fix is a source the billing system does not control, such as the signed contract for the price or a separate count from the servers that handled the API calls.

Even then, be careful. Two copies of the same broken log are still one broken log. Saving it twice does not make it independent.

How did I check the results?

An automated test suite of 19 tests passed. It checks the bill math, the correct cases, every caught error, the two errors that got through and the dollar impact of the main errors. I reran everything from a fresh download and got identical results. This proves the code behaves as described. It does not replace a finance review.

I recorded the results on October 9, 2026, using Python 3.13 and SQLite 3.45, then reran them on Python 3.10 to confirm the minimum version works. All 38 results matched.

The expected bill comes from SQL, while the bills in each scenario come from separate Python code, so one cannot quietly copy the other. The tests also do simple math independently, such as $50 plus 2,000 API calls at $0.02. For the scenario where two errors cancel out, the tests confirm the report shows $10 underbilled and $10 overbilled instead of a misleading zero.

Two of the payment checks, an unexpected payment with no matching bill and a payment larger than its bill, are not triggered by any scenario. Two extra tests trigger them directly, so no check goes untested.

All data is made up. No real billing system, payment provider or bank was used, and no finance, billing or product team reviewed the results.

Who should fix each billing problem?

Each flagged issue needs one owner who can confirm the facts and decide the fix. Finance owns what customers were entitled to and what they still owe. Billing operations owns corrected bills and customer messages. Product owns missing usage, and data engineering fixes broken data feeds. Finding a problem does not authorize a charge or a refund.

Problem Where to look first Question to answer
Customer never billed Subscription list, plan status and billing run Did this customer owe us money this month?
Usage doesn’t match Product usage log and billing usage log Are API calls missing, late, duplicated or on the wrong customer?
Wrong price Signed contract and price list in effect Which price applied this month?
Canceled customer billed Cancellation date and plan history Does this bill need to be corrected?
Bill sent to wrong customer Account owner and billing contact Who should receive and pay this bill?
Bill overdue Payments, credits and due date Should collections follow up?
Marked paid, no payment Payment provider and account credits Is the payment record missing, or is the status wrong?

These are the owners I would propose. The experiment does not run those reviews.

For a leadership dashboard, I would show confirmed missing charges, overcharges, overdue bills and open issues as separate numbers. I would add how long issues take to fix and which root causes keep coming back. A flagged issue only counts as recovered money once the fix is approved and the money shows up.

That is the same principle behind The Data-Driven Executive: every metric needs a clear meaning and a decision attached to it.

Would I use this approach for real subscription billing?

Yes, as a starting point. The line-by-line comparisons are the right foundation. A real business would add its actual pricing rules, a usage source the billing system cannot change and a clear process for working through flagged issues. This example skips partial months, credits, refunds, taxes, late usage and annual plans.

The systems I reconciled in my own work were far larger than six CSV files, but the questions were the same. Is this customer active? What did they order? What did we bill? What did they pay?

Timing matters too. Stripe notes that usage is processed asynchronously, so recent API calls may not show up right away. A gap before the agreed cutoff might just be a delay. The same gap after the cutoff needs escalation. This example uses finished month-end data and skips that problem.

My next step would be an agreed cutoff for when usage counts as complete, plus a queue where a person reviews and closes each flagged issue. I would check the results against signed contracts and an independent usage count before letting any of this change a customer’s bill.

I would also relax the one-bill-per-month rule if the business legitimately sends several bills in a month. A check that does not match how the business really works creates false alarms, and people learn to ignore it.

Official resources

What are you building, which subscription or usage data do you have, and what is stopping you from checking the bills?

Leave a Comment

Your email address will not be published. Required fields are marked *