Every month, the finance team at a mid-market SaaS company ran the same sprint.

  1. Day one: export open contracts from a Google Sheet maintained by the RevOps team, because the Salesforce data was incomplete after a migration two years ago. Pull raw usage records from the product database via a CSV an engineer generated manually. Cross-reference both against NetSuite to find which customers hit usage tiers, which had active credit memos, and which had custom discounts buried in a contract amendment from six months ago.

  2. Day two: the engineer delivers a corrected CSV. The first export had duplicate event rows for one customer, which inflated their API call count by 11,000 requests. Back to cross-referencing.

  3. Day three: build the draft invoice spreadsheet. QA pass. Catch a second error: a customer whose contract amendment from two months ago had a different ID format than the one in NetSuite, so the discount did not apply.

  4. Day four: key the invoices into NetSuite. Send. Field two emails from customers querying their amounts. One was correct but confusing. One was wrong.

Four days. Every month. For a team of three people who had better things to do.

Live Data Sources 3 connected
Google Sheets
Google Sheets
Contract terms · 47 active rows · 3 amendments since last sync
✓ Synced
NetSuite
NetSuite
Customer master · Open AR · 12 credit memos · 89 customers
✓ Synced
Postgres
Postgres
API event logs · 1,247,832 events · May 2026
✓ Synced
Enso Reconciliation Engine
312 billing lines normalized · 4 exceptions flagged for review
May 2026 Reconciliation 4 need review
85 invoices ready · 4 flagged for human review
CustomerUsageInvoice AmtStatus
Acme Corp247k API calls$12,350.00Ready ✓
Globex Ltd84k API calls$4,210.05Ready ✓
Initech LLC193k API calls$9,920.00⚠ Tier crossover
Umbrella Co3.2k API calls$500.00Min. commit ✓
Exception: Initech LLC — Mid-Month Tier Crossover

Usage crossed from Growth into Enterprise tier on May 19. Contract amendment amd_itc_03 specifies proration on crossover. Enso has calculated a split invoice: 18 days at Growth rate ($0.05/req) + 13 days at Enterprise rate ($0.042/req).

View amendment ↗
Push to NetSuite ✦ 85 invoices ready
Human review complete — 85 invoices approved
Push Summary
Invoices85 draft entries
Total billed$847,200.00
Credits applied$3,400.00 across 4 customers
Exceptions resolved4 of 4
DestinationNetSuite — Draft Invoice queue
Preview all 85 ↗
Pushing to NetSuite draft queue...
Done. 85 invoices created. Time elapsed: 14 seconds.

The Three-System Problem#

To produce one invoice, the finance team needed three separate systems, none of which talked to each other.

  1. NetSuite held the customer master, open invoices, credit memos, and ERP IDs. It was the system of record, but it did not know about usage.

  2. Google Sheets held the active contract terms: committed usage tiers, custom discounts, and amendment history going back three years. The sheet existed because the deals were complex enough that a standard Salesforce contract field could not capture the nuance. Every account manager had added rows.

  3. A Postgres database held raw event logs from the product. Every API call, every usage event, timestamped and stored. Getting anything useful out of it required a SQL query and an engineer willing to run one on request.

Nobody could open a single screen and see what a customer owed. A person had to assemble that number by hand, every month, across all three.

What Connecting the Sources Actually Looked Like#

Enso connected to all three in the same day. NetSuite and Postgres were credential-based. Google Sheets required a one-time share with the Enso service account.

The first thing Enso surfaced: the customer ID formats did not match. NetSuite used numeric IDs. The Google Sheet used a company name slug the account team had invented. Postgres used a UUID from the product’s user table. Three systems, three formats.

Enso ran a fuzzy match pass on first sync and presented a mapping table for human confirmation. The finance lead spent 20 minutes reviewing 89 matches, correcting 4 where the fuzzy match was wrong, and confirming the rest. That mapping became the permanent translation layer.

After that, the nightly sync ran automatically.

The Reconciliation Logic#

Billing at a usage-based SaaS company is not a lookup. It is a calculation with branches.

A customer billed at $0.05 per API call with a $500 monthly minimum and a 15% discount for contracts over $5,000 per month requires several checks before a number can be put on an invoice: total usage from the product database, applicable tier from the contract sheet, minimum commit check, discount eligibility based on projected total, and any open credit memos from NetSuite.

Enso executes these checks deterministically, in sequence, for every customer. The logic is written once, reviewed by the finance team, and runs the same way every time.

The output is a reconciliation view: every customer, their usage, the tier applied, the invoice amount, and a status. Exceptions get flagged with a reason. The team does not need to search for problems; they appear at the top of the queue with enough context to resolve them without opening NetSuite, the spreadsheet, or the database separately.

The Exceptions That Still Need a Human#

Automation handles the straightforward cases. The cases that still need judgment are the ones where the data is technically correct but the right answer requires knowing something about the customer relationship.

The most common exception in this team’s workflow: a customer who crossed a usage tier mid-month. Their contract specified proration on crossover. Enso calculated the split automatically and flagged it for confirmation before pushing to NetSuite, because a split invoice looks unusual to the customer and the finance team wanted to make sure the calculation matched what sales had promised verbally.

Other common exceptions: customers who paused usage mid-month after a support incident and expected a credit, or customers whose contract amendment was sitting in the sheet but had not yet been signed. Enso surfaces these. The human decides. Then approves.

That review took 30 minutes in month one. By month three, with the exception patterns becoming familiar, it was closer to 15.

What Changed, and What Did Not#

NetSuite is still the ERP. The Google Sheet is still how the RevOps team manages contract terms. The Postgres database still logs every event.

What changed: nobody manually exports, cross-references, or keys data anymore. The reconciliation happens overnight. The finance team arrives on the first of the month to a queue of 85 invoices ready to approve and 4 exceptions to review, rather than four days of work ahead of them.

The engineer who used to run the CSV export on request does not do that anymore. That was not a small ask, it happened 12 times a year.

BeforeAfter
Time to close billing4 days30 minutes of review
Engineer dependencyMonthly CSV exportNone
Billing errors caught post-send4 to 6 per month0 in first quarter
Customer ID mismatchesFound manually, occasionally missedFlagged on first sync, permanent mapping
Amendment trackingManually maintained in spreadsheetVersioned, diffed on each sync
Total hours per month22 hours across the team2 hours

Why This Works for Complex Billing#

The reason this kind of automation fails when teams try to build it themselves in scripts or Zapier workflows is versioning.

Billing rules change. Customers renegotiate. A script that calculates invoices correctly in January may produce wrong numbers in April after three amendments without anyone noticing. There is no diff. There is no audit trail.

Enso versions the contract rules. When the Google Sheet changes, Enso shows what changed, when, and which customers are affected. The finance team approves the rule change before it applies to live billing. Transactions that ran before the amendment date stay on the old version.

That is the thing that makes finance teams comfortable enough to stop running the manual backup. The audit trail exists. The logic is inspectable. And if a customer disputes an invoice, the team can show exactly which rule version produced that number and when it was approved.

What’s Next#

The billing reconciliation fixed one problem. It also changed what the team thought was possible.

The Google Sheet holding contract terms is the last manual piece in the chain. It works, but it carries the same fragility the original process had: one person’s naming convention, one wrong row, one amendment that did not get saved. The team is now migrating those contract terms into Enso directly, so billing rules are owned and versioned by the system that runs them.

The second project: collections. Invoices used to go out four days late every month. Now they go out on the first. That freed up enough capacity to actually act on overdue accounts before they age. The team is building a dunning workflow in Enso that pulls AR aging data from NetSuite and sends sequenced follow-ups automatically, with a manual hold list for accounts the sales team is actively managing.

The pattern is the same in both cases: find the spreadsheet doing mission-critical work, and replace it with something that runs reliably and leaves an audit trail.


The team spent four days a month on billing reconciliation because the data lived in three systems with no shared schema, no normalized IDs, and no automated path between them.

Connecting those systems took one day. The reconciliation has run automatically every month since. If your billing process looks anything like this, see how Enso handles usage-based billing or get a demo with your own data .