We usually blog about the magic of Ruby or Elixir, performance and errors. Once in a while, we write about how we run AppSignal as a business, and how we solved technical challenges we ran into.
One of our most popular posts was about handling VAT in Stripe. We wrote about how we handle billing and EU VAT by using Stripe and Moneybird. Over the years, we have evolved our approach a lot and a lot of people have asked us to share those steps. So here we go!
Running a SaaS billing platform is not part of the core value we provide as a business, so we try to leverage existing tools as much as possible. We do all our billing and bookkeeping in Moneybird. In this article, we'll show how we handle EU VAT, and in an upcoming one, we'll provide some insight into our bookkeeping setup. We hope these will be useful in helping you to build and run a product business.
VATMOSS and VAT Deferred
Since 2015, the EU requires vendors of digital services to charge the VAT percentage of the country where the service is delivered. For example, if you have a customer in Germany you have to charge 19% VAT, and for one from the Netherlands, that's 21%. If your customer is a business and supplies a valid VAT number, the VAT can be deferred. In practice, this means that you don't charge the customer any VAT, but that the customer instead pays the VAT in their own country (which saves them from filing VAT returns in multiple countries).
Luckily, Moneybird can handle a lot of the administrative details if you create your invoices with all the details set correctly. So how do we do this?
Stripe's Webhooks
We leverage Stripe's subscription system and listen to a number of webhooks. Together they fully handle a customer's payment with the right VAT percentage:
invoice.created
Whenever a new invoice is created, we create a draft invoice in Moneybird and perform a VAT number check for EU based customers. Stripe creates the invoice 48 hours before it's sent, so there is a large enough window to make any required changes.
Customers in the EU can enter their VAT number when entering their billing details on appsignal.com, which allows us to defer VAT. We check the format of the number on submit and validate the VAT number with the VIES service every time an invoice is created. For this, we use the invaluable valvat
gem.
The code we trigger looks like this:
1if account.vat_number.present? && account.in_eu_country?
2 # Validate VAT number for new invoice using the valvat gem
3 VatNumberValidation.perform(account)
4end
5Moneybird.sync_contact(account)
6Moneybird.create_invoice(account, invoice)
If the VAT check indicates that a valid number was entered, we sync contact information and pay the invoice. If the VAT validation fails, we return a 500 status code. In that case, Stripe will not proceed with moving the invoice to the next stage and will instead retry a few hours later.
If the VAT validation indicates that the number is no longer correct, we modify the VAT percentage on the invoice to the correct amount and inform the customer that their VAT number is incorrect. If the VAT number is correct we can set the VAT percentage in Stripe to 0%.
The Moneybird
class is a very simple wrapper around the API we built ourselves using RestClient. Creating an invoice looks like this:
1def create_invoice(account, stripe_invoice)
2 RestClient.post(
3 api_url('sales_invoices.json'),
4 invoice_hash(account, stripe_invoice).to_json,
5 api_headers
6 )
7end
The invoice_hash
method builds a hash according to the Moneybird's specifications. The trick is in using the correct VAT percentage for every situation. We have a constant containing ids of VAT percentage and categories in Moneybird and set a corresponding VAT percentage for every EU country.
1MONEYBIRD_CONFIG = {
2 :standard_workflow_id => 00000000,
3 :vat_deferred_workflow_id => 00000000,
4 :stripe_invoice_id_custom_field_id => 00000000,
5 :vat_reverse_charged_tax_rate_id => 00000000,
6 :no_vat_tax_rate_id => 00000000,
7 :vat_tax_rate_ids => {
8 :de => 00000000,
9 :fr => 00000000,
10 ...
11 }
12}
You then determine the correct VAT percentage like so:
1tax_rate = if account.vat_complicit?
2 MONEYBIRD_CONFIG[:vat_tax_rate_ids][account.country_code]
3 elsif account.vat_reverse_charged?
4 MONEYBIRD_CONFIG[:vat_reverse_charged_tax_rate_id]
5 else
6 MONEYBIRD_CONFIG[:no_vat_tax_rate_id]
7 end
This id can then be used when adding an invoice line. Now we just have to wait for the charge to succeed.
invoice.payment_succeeded
If the charge succeeds, we reset any failed payment state on the account, register payment, and have Moneybird send an email with a PDF invoice:
1account.has_no_failed_payment!
2
3Moneybird.register_payment(account, invoice)
4Moneybird.send_invoice(account, invoice)
invoice.payment_failed
If payment fails, we set the customer's account to a "has failed payment" state and send an email to the account owners (or their chosen alternative email address) to let them know there was a problem with the charge.
1account.has_failed_payment!
2Moneybird.send_invoice_if_not_sent_yet(account, invoice)
3AccountMailer.delay.payment_failed(account.id, message)
4AppsignalMailer.delay.payment_failed(account.id, message)
Stripe retries charging a couple of times, depending on your settings. This logic will get called multiple times and can keep track of the number of attempts.
Conclusion
We are here to help you get amazing insight into your applications. That also means we are not here to build another billing and administrative system ;-). We leverage other tools as much as we can. In this article, we just showed you how we use Stripe and MoneyBird to deal with VAT.
We walked you through how we use Stripe for subscriptions and payment, and Moneybird for invoicing and financial administration. Through this approach, we end up with invoices that are set up in a way that we can use Moneybird's VAT features to perform correct filing, leaving us free to focus on running a SAAS business.
We've kept it a bit more simple than reality though. In today's post, we skipped over how we do all the required bookings in our administration. We'll dive into that in a separate post.