Handling VAT with Stripe

Thijs Cadier

Thijs Cadier on

Are you a European company offering subscription based services, while using Stripe to handle payments? Right... VAT. Here's how we fixed it.

A disadvantage of being based in Europe is having to comply with EU VAT regulations. It basically boils down to:

  • Your customer is based in the same country as you: charge your local VAT tariff.
  • Your customer is based in another EU country than you and has a valid VAT number: don't charge VAT.
  • Your customer is based in another EU country than you, but is a private person or company without a valid VAT number: charge your local VAT tariff.
  • Your customer is based outside of the EU: don't charge VAT.

To complicate matters some more, the burden of proof is on the supplier. You have to make you sure you know the registered business addresses of all customers. You must also validate their VAT numbers using the EU's VIES check site.

Of course, a SaaS business wants to automate all of the above. All of our monthly or yearly generated invoices should only include VAT when necessary. Until somewhere last year there were little to no credit card payment gateway providers that were either aware of the problem, cared enough about it to fix it, or offer hooks for clients to add their own logic for calculating VAT. That's until Stripe launched in the Netherlands, and we came up with a pretty clean workaround that's possible because of the hooks their system offers.

Stripe offers an invoice.created web hook. This hook gets called when a customer is ready for its next billing cycle. After an invoice is created you have a chance to modify the invoice before an actual charge is made, by telling Stripe to call a webhook. We use this to either add VAT, or to let Stripe know that the invoice is okay is as.

To start things off, we created a simple class to handle the webhooks that are sent by Stripe. This is called by a controller that receives the web hook call:

class StripeEvent attr_reader :event_id def initialize(event_id) @event_id = event_id end def event @event ||= Stripe::Event.retrieve(event_id) end def handle case event.type when 'invoice.created' invoice_created when 'invoice.payment_succeeded' invoice_payment_succeeded end end end

For the invoice_created hook, a method gets called that takes the total amount of the invoice, including any discounts or prorations, and adds 21% VAT to it:

def invoice_created invoice = account = Account.where(:stripe_customer_id => invoice.customer).first sync_contact(account) if !invoice.closed && account && account.has_to_pay_vat? Stripe::InvoiceItem.create( :customer => invoice.customer, :amount => ( * 0.21).to_i, :description => 'VAT', :currency => invoice.currency, :invoice => ) end end

account.has_to_pay_vat? checks wether the customer is in a country for which we (potentially) have to charge VAT, as well as the validity of their VAT number. We get this data through the contact sync with MoneyBird, (a great Dutch invoicing system we’ve been using for years):

def has_to_pay_vat? country == 'nl' || (eu_countries.include?(country) && !valid_vat_number?) end

Stripe processes our response and charges a credit card, after which we receive another webhook to let us know we received some money. We then have all the information we need to create an invoice on our end:

def invoice_payment_succeeded invoice = account = Account.where(:stripe_customer_id => invoice.customer).first create_invoice(account, invoice) end

That wraps it up. We added VAT if necessary and created an invoice if all went well. So is this system perfect? By no means. Since we get a total amount including VAT, there sometimes are one-cent rounding differences between MoneyBird and Stripe. Also, the VAT amount gets added before coupons are applied. Therefore we can’t use the line items to make a fully correct invoice in MoneyBird, but have to work backwards from the total amount.

Stripe could do a couple of things to improve the situation (from comprehensive to simple):

  • Implement a full EU VAT system, including checking whether VAT numbers are valid and so on. Would be awesome, but possibly quite some work for them.
  • Add a VAT percentage field on the customer level which would be used to do all the calculations, with the vendor being responsible for a correct value.
  • Provide a way to add the VAT amount as the very last item in an invoice, so there's at least a reliable way to prevent rounding errors.

For the moment we're really happy that we made this work. Kudos to Stripe for making it possible with their great API, though we do hope this ordeal gets some more love from them in the future :-)

Update: This situation has much improved since writing this post, read our follow-up.

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