Double-Entry Bookkeeping for US Healthcare

This article is written for users in the US.

Healthcare accounting in the US can get messy: you visit a provider, insurance processes a claim, and weeks later you receive an EOB (Explanation of Benefits) showing how much you actually owe.

Below is a practical model for how to represent this flow in your ledger — including payment plans, per-person tracking, and automation ideas.

Step One: Record the EOB

When the EOB arrives, you know:

  • The amount billed by the provider
  • What insurance covered
  • What you owe (your share)

At this stage, you’ve incurred an expense and a liability to the provider.

2025-11-01 * "Dr. Smith Clinic - Office Visit" "EOB from Blue Cross"
  Expenses:Healthcare:OOPCovered:Medical:Rocky       500 USD
  Liabilities:Healthcare:OOPCovered:Medical:Rocky

You now owe $500 to the provider.

I stick the deductible/coinsurance split (from the EOB csv import) into metadata so I can verify these against what the insurance provider says:

2025-11-01 * "Dr. Smith Clinic - Office Visit" "EOB from Blue Cross"
  deductible: 400
  coinsurance: 100
  Expenses:Healthcare:OOPCovered:Medical:Rocky       500 USD
  Liabilities:Healthcare:OOPCovered:Medical:Rocky

Step Two: Set Up a Payment Plan (Optional)

Some providers let you pay over time with no interest, and it makes sense to sign up for these. This effectively moves your balance from one liability account to another.

2025-11-10 * "Set up payment plan with Dr. Smith Clinic"
  Liabilities:Healthcare:OOPCovered:Medical:Rocky      500 USD
  Liabilities:Healthcare:PaymentPlan:Smith-Clinic     -500 USD

Now the short-term liability to the provider is gone, replaced with a longer-term liability under PaymentPlan.

Step Three: Make Direct Payments or Monthly Payments

Each monthly payment reduces the payment-plan liability.

2025-12-01 * "Payment Plan Installment - Dr. Smith Clinic"
  Liabilities:Healthcare:PaymentPlan:Smith-Clinic      50 USD
  Liabilities:Credit-Card:Chase                       -50 USD

If you pay the provider directly (no plan), simply clear the OOPCovered liability instead:

2025-11-05 * "Payment to Dr. Smith Clinic (no plan)"
  Liabilities:Healthcare:OOPCovered:Medical:Rocky      500 USD
  Liabilities:Credit-Card:Chase                       -500 USD

Account Flow Summary

Step From To
EOB Received Expenses:Healthcare:OOPCovered:... Liabilities:Healthcare:OOPCovered:...
Payment Plan Setup Liabilities:Healthcare:OOPCovered:... Liabilities:Healthcare:PaymentPlan:...
Payment (Credit Card) Liabilities:Healthcare:PaymentPlan:... Liabilities:Credit-Card:Chase
Direct Payment (No Plan) Liabilities:Healthcare:OOPCovered:... Liabilities:Credit-Card:Chase

Tracking by Person

For households with multiple people on one insurance plan, create per-person subaccounts under both Expenses and Liabilities. This keeps reporting and querying clear, and easy to debug.

2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Medical:Person1   USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Pharmacy:Person1  USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Medical:Person2   USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Pharmacy:Person2  USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Medical:Person3   USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Medical:Pharmacy:Person3  USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Dental:Person1  USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Dental:Person2  USD
2020-01-01 open Liabilities:Healthcare:OOPCovered:Dental:Person3  USD

Automation

Automating EOB Imports

Many insurers now let you download EOBs as CSVs, and if your employer provides an aggregator service, that can even merge your medical and dental plans into one file. A well-written importer can parse these CSVs to automatically generate transactions like:

2025-11-01 * "EOB: Blue Cross - Dr. Smith Clinic"
  Expenses:Healthcare:OOPCovered:Medical:Rocky    500 USD
  Liabilities:Healthcare:OOPCovered:Medical:Rocky

The CSV usually includes:

  • Patient name
  • Provider and service details
  • Allowed amount, insurance payment, and member responsibility
  • Copay, coinsurance, deductible breakdowns

That means step 1 (EOB entry) can be fully automated, and step 3 (credit-card payment) is likely already covered by your importer. The only occasional manual entry is a payment plan, which happens rarely. I find that automation is the key to running your Beancount system effortlessly.

Beancount Plugins

  • Use bean-download’s [needs_update_days](https://reds-rants.netlify.app/personal-finance/how-up-to-date-are-my-accounts/) on the Liabilities:Healthcare:OOPCovered:Medical:Rocky:* accounts to warn you when you haven’t imported your EOBs in a while. This can warn you of bills you are unaware of
  • Use zerosum to automatically move pairs of matching transactions in Liabilities:Healthcare:OOPCovered:Medical:Rocky:* to a different account (eg: to Liabilities:Healthcare:OOPCovered:ZS-Matched:Medical:Rocky:*) so you can focus on the remaining few, which are the transactions needing attention

Automatic Classification

Smart_importer uses the description to distinguish between transactions on a payment plan and those being paid directly. I occasionally have to fix it, or if using per-person accounts, add that part of the account.

Notes

With all the above in place, the only manual intervention needed is when I start or change a payment plan, in addition to the occasional classification fix.

Balance Assertions

Assert balances in Beancount to catch problems. Insurance cycles are annual. So:

  1. At the end of every year (or shortly thereafter), the Liabilities:Healthcare:OOPCovered:* accounts are expected to be zero. Assert these. I book non-insurance related healthcare expenses like over-the-counter supplies to a separate account (eg: Expenses:Healthcare:Noncovered:OTC) so that the insurance related accounts are kept separate, and the assertions above are easily made.
  2. Deductibles, and out of pocket max if you hit it are on displayed on your insurance website or in your imported csv, and you can quickly compare your Expense account for the year against these.
  3. Your payment plan shows the outstanding balance. I occasionally stick in a balance assertion from this (manually) just to ensure there are no surprises

A Note on Simplifying

I simplify my accounting by ignoring how much the insurance company paid the provider — that part isn’t really relevant to me. I recommend this approach: it keeps the accounting simpler without losing anything meaningful.

If you do want to track it, simply book the insurance payment as Income:Insurance in step 1. One advantage of tracking this is that you can import all your medical appointment history — even visits you didn’t pay for — making your Beancount journal a searchable record of your entire healthcare history.

Reporting / Analyzing

  • Your annual healthcare expenses are now easily queryable in Beancount or viewable in Fava simply by looking at the Expense:Healthcare account for the year or other time period
    • Compare annual expenses, and look up out of pocket totals for any year with the Expense account
  • The balance in Liabilities:Healthcare:OOPCovered is the amount you will have to pay shortly (expect bills for this amount)*
    • This has helped me find missing bills, bills sent to the wrong address, bills I forgot to pay, and such. I originally started tracking these to avoid the negative consequences of missing a payment
  • The balance in Liabilities:Healthcare:PaymentPlan is what you have committed to paying over time
  • Of course, you can slice all this by person, and type (medical, pharmaceutical, dental) trivially

In Summary

By modeling healthcare bills as expenses flowing through liabilities — and optionally through payment plans — you maintain a precise, chronological, double-entry record of every step:

  1. EOB creates the expense and liability
  2. Payment plan refinances that liability
  3. Payment clears it
  4. Per-person accounts keep things organized
  5. Automation ties it all together

With EOBs imported, I find myself grepping my ledger to answer questions like “when did I last get a dental cleaning?” and such.

Your Beancount file becomes a faithful mirror of your healthcare finances — transparent, queryable, and ready for Fava dashboards.

Notes mentioning this note

There are no notes linking to this note.