If you're writing your own code to push Stripe payments into QuickBooks Online or Xero, the first version usually works for a plain $100 sale. The trouble shows up weeks later, when the bank feed stops matching. Here are four cases worth handling before that happens, with the journal entries that keep the books right.
1. Stripe pays you net, in batches
Stripe takes its fee before it pays you, and one payout can bundle charges, refunds and fees from several days. If each sale goes straight to the bank account at its full amount, nothing will ever match the deposit.
Post each charge into a Stripe clearing account instead, at its full amount, with the fee split out:
Dr Stripe Clearing $96.80
Dr Stripe Fees $3.20
Cr Sales $100.00
Each payout then moves money from the clearing account to the bank:
Dr Bank $5,000.00
Cr Stripe Clearing $5,000.00
The payout entry now matches the bank deposit, and at month end the clearing account should equal your Stripe balance. If you need to know what went into a payout, Stripe's API can list the balance transactions for an automatic payout by its ID.
2. A refund on a yearly plan that's partly used
A customer pays $1,200 for a year. You hold it as deferred revenue and release $100 a month. Three months in, they cancel and you refund the remaining $900.
The easy mistake is posting the $900 as a refund against revenue. Only $300 was ever counted as revenue, so net revenue for this customer drops to minus $600 even though they received $300 of service, and the $900 sits in deferred revenue forever.
The refund should come out of deferred revenue first:
Dr Deferred Revenue $900.00
Cr Stripe Clearing $900.00
Only the part beyond what's still deferred is a refund of earned revenue. To get this right, your code has to know how much is still deferred. That's a fact about your books, not something in the Stripe event, so you need to keep the schedule somewhere.
3. A dispute where the fee is bigger than the charge
When a customer disputes a charge, Stripe pulls the disputed amount plus a dispute fee from your balance. You might still win, so the disputed amount belongs in a receivable until the outcome. The fee is gone either way, so it's an expense now:
Dr Disputes Receivable $100.00
Dr Dispute Fees $15.00
Cr Stripe Clearing $115.00
If you win, the $100 moves back from the receivable to the clearing account. If you lose, it becomes an expense.
The quiet bug is telling the two amounts apart by size. On a $9.99 charge, the $15 fee is the bigger number. Use the fee details Stripe puts on the dispute's balance transaction instead of guessing from which amount is larger.
4. Partial refunds on a taxed sale
If the sale collected sales tax, each refund has to reduce the tax you owe as well, in proportion. Round each partial refund on its own and a sale refunded in three pieces can leave a cent or two of tax owed on a sale that no longer exists. Work out the tax on the total refunded so far, then subtract what earlier refunds already took back. The pieces then add up to exactly the tax you collected.
The library that came out of this
These all came up while building Ledgerly, a free, open-source TypeScript library that turns Stripe events into balanced QuickBooks Online and Xero journal entries. You call mapEvent(event) in your own webhook handler and post the result. The cases above, plus payouts and currency changes, are checked by tests. The ones that need saved history, like the yearly plan refund, are handled by an optional receiver that keeps that history.
To see what it would post for your own account without connecting anything, there's a read-only preview. Create a restricted Stripe key with Read access, then run:
STRIPE_SECRET_KEY=rk_live_... npx --yes ledgerly@0.18.1 --stripe --days 30
The README shows how to enter the key without it landing in your shell history. The code is at https://github.com/jakethehoffer/ledgerly. AI helped build it and draft this post. It creates journal entries, not invoices, and it isn't accounting advice.
If you've written this kind of code yourself, which case caught you out?
Top comments (0)