Zero-touch SaaS Bookkeeping with Stripe and Moneybird

Thijs Cadier

Thijs Cadier on

Zero-touch SaaS Bookkeeping with Stripe and Moneybird

In today's post, we will show you our full process for booking Stripe payments for our SaaS business in our accounting software.

I often get asked how we do the bookkeeping side of our business by other founders from our neck of the woods. We are based in the EU, so we have to deal with more stringent requirements for keeping our books than companies in other parts of the world.

To fully comply, we need to connect the payment for our invoices to an actual bank transaction. Preferably on an individual level. This is, or could be, as complex as it sounds. Doing that puzzle manually for every payment is very time-consuming. But since we have a sizable aversion to manual work we automated it all 😃.

We've blogged about how we handle VAT in the past. But that's just one part of the puzzle. To satisfy your accountant, and eventually the tax authorities, you need to go a lot further.

This post is for people running or starting a SaaS business dealing with the same requirements. If you're not in that group, it's fine to stop reading.

We use an amazing bookkeeping product called Moneybird. The same accounting principles described here should work in other bookkeeping products too, though.

I will now cover all steps that lead to a completed booking of an invoice for a subscription.

Disclaimer: I am not an accountant and am just sharing how we approach this. Make sure to verify this approach with your own accountant.

An Invoice is Created

Whenever Stripe creates a draft invoice for one of our customer's subscriptions, it fires an invoice.created event to our webhook. At that point, we create a draft invoice in Moneybird:

def create_invoice(account, stripe_invoice) api_url('sales_invoices.json'), invoice_hash(account, stripe_invoice).to_json, api_headers ) end

Our blog post about VAT covers some more detail on that aspect.

Tip: Use periods in Moneybird when creating invoices. This way, revenue will be evened out over the months automatically. Some complex and expensive work your accountant needs to do every year is not necessary anymore then.

Invoice Payment Succeeds or Fails

A couple of days later, Stripe tries to collect payment for the invoice. This can succeed or fail. In both cases, we set the invoice state to sent. If the payment succeeds, we manually register it.

def invoice_payment_succeeded invoice = @account = get_account(invoice.customer) Moneybird.register_payment(@account, invoice) Moneybird.send_invoice(@account, invoice) end

Moneybird then changes the state of the invoice, but it does not consider it truly paid until the invoice is connected to a real bank transaction. So we are half-way done now.

The Payout Containing an Invoice is Created

Another number of days later, Stripe creates a payout that includes the payment for this invoice. It fires a payout.created event.

We're now getting to the magic that makes our approach tick. We consider our Stripe balance to be the equivalent of a bank account. We simply recreate all payouts as financial bank statements on the bookkeeping side:

def payout_created payout = Stripe::Payout.retrieve(@payout_id) balance = full_stripe_balance # Fetches entire balance for this payout financial_mutations_attributes = {} balance.each_with_index do |line, i| financial_mutations_attributes[i + 1] = { "date" =>"%d-%m-%Y"), "message" =>, "amount" => } end financial_statement = { "financial_statement" => { "financial_account_id" => 00000, "reference" => payout_id, "official_date" =>"%d-%m-%Y"), "official_balance" => "0", "financial_mutations_attributes" => financial_mutations_attributes } } Moneybird.create_financial_statement(financial_statement) end

We end up with a financial statement a line for every payment. The amount for the payment does not include Stripe's fee. We will see why that's important in the next step.

The Payout Containing the Invoice is Transferred to Our Bank Account

If all goes well, the payout is transferred to our bank account a little while later. Stripe fires a payout.paid event. The following bookings tie everything together.

We loop through the Moneybird financial statement and fetch the right one from the Stripe payout. Remember that there is a difference between the transaction's amount and the invoice's amount: The fees have already been deducted.

Let's use an example here:

We have an invoice with a total amount of EUR 50. The Stripe fee was EUR 1. The invoice is in our local currency (currency conversion is another topic that I'm leaving out of scope for this post).

We then find the Moneybird invoice for that line:

mb_invoice = Moneybird.find_invoice(account.new_moneybird_contact_id, invoice)

Next up, we link up the booking to the invoice. We use the total amount for the invoice, EUR 50, in this case.

Moneybird.link_booking( mutation["id"], "booking_type" => "SalesInvoice", "booking_id" => mb_invoice["id"], "price" => invoice.amount_due, "price_base" => line.amount )

The invoice is now considered fully paid by Moneybird: The full amount was booked against a financial transaction.

This leaves the financial transaction in an unresolved state though. Its amount was EUR 49, we booked 50 against it. So its internal balance is now at EUR -1.

We resolve this with our final booking step. We have a normal cost category for all Stripe fees. We book the remaining negative amount on it:

Moneybird.link_booking( mutation["id"], "booking_type" => "LedgerAccount", "booking_id" => 9999, "price_base" => fee.amount )

The payout also includes a line with the total amount that's transferred. We book both this transaction, and the equivalent transaction on our bank account against crossposts. This evens everything out to zero.

There you have it:

  • The invoice is paid and booked against a financial statement.
  • Stripe fees are booked on a regular cost account and show up in the right place in your financial reporting.
  • All fees are directly booked on the cost account, and VAT is handled with the invoices. We don't need to book Stripe's invoice.
  • The transfer of money is booked against crossposts, so it ends with a balance of 0.
  • No manual labor involved in any of this.

Yearly Report Time

At the beginning of the next year, it's time for our accountant to start work on our books. They do most of their work within Moneybird and make sure any remaining inconsistencies are corrected.

Moneybird has a function that can create a standard audit file export. Our accountant uses that export to load all data into the software they use to do their audits and generate a yearly report.


That's it: Zero-touch SAAS bookkeeping while complying fully with the tax authority's requirements. Let me know if I can help with some more code samples!

Thijs Cadier

Thijs Cadier

Thijs is a co-founder of AppSignal who sometimes goes missing for months on end to work on our infrastructure. Makes sure our billions of requests are handled correctly. Holds the award for best drummer in the company.

All articles by Thijs Cadier

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps