Managing EU VAT with Stripe for a SaaS is not *that* hard

This is a short follow up to last week's story on Stripe Billing as a reader on Hackernews commented that it seemed we didn't handle VAT.  We do, but I just left it out of the story.

For those not familiar with handling EU VAT for SaaS companies: It's a bit of counter intuitive jungle. At least that's what some dedicated SaaS startups make you believe. Also, Stripe does not handle it at all. They give you a  { taxRate: null } field for you to fill.

Turns out it's not super hard if you take some time to dig into it. This doc by Chargebee is a really good primer. But not everyone wants to use a third party service. We use a (very) nice NPM package and some fairly trivial Javascript code to integrate it.

Might save you $50 a month or more. Thats two beers in San Francisco!

!! Big fat disclaimer !!

I'm not a lawyer, nor an accountant. Nor do I want to disregard some of the companies specifically offering this feature like Octobat, Quaderno and Chargebee. Go for them if you don't want to code it yourself. Totally legit businesses.

DIY sales tax service

Not really DIY, but more an integration of an existing package, the glorious Sales Tax Node.js package. There might be something similar for your language/platform of choice.

In the piece of slightly redacted code below, you can see what happens when a Stripe webhook comes in and how we deal with the sales tax on the back end. It is an interplay between two services.

  1. Stripe, for subscriptions & payments
  2. The Sales Tax NPM package, for VAT calculation based on country and VAT number.
function updateSubscription (stripeSubscriptionId, plan, interval, details) {
  SalesTaxService.getSalesTax(details.country, details.vatNumber)
    .then(salesTax => {
      const taxPercent = SalesTaxService.determineRate(salesTax)
      return StripeService.updateSubscription(stripeSubscriptionId, {
        plan: stripePlanId(plan.planId, interval),
        tax_percent: taxPercent
      })
    })
    .then(subscription => {
      return Account
        .query()
        .update({
          planId: plan.id,
          planInterval: interval,
          ...
        })
        .where({id: accountId})
    })
}

The crucial things are the the country field and the vatNumber field. They determine whether a customer is either a B2B or B2C customer and if the customer is inside or outside of the EU. We feed that data to our SalesTaxService wrapper, which looks as follows.

const config = require('config')
const SalesTax = require('sales-tax')

SalesTax.setTaxOriginCountry(config.salesOriginCountry)

class SalesTaxService {
  static getSalesTax (countryCode, vatNumber) {
    return SalesTax.getSalesTax(countryCode, null, vatNumber)
  }

  static validateVatNumber (countryCode, vatNumber) {
    return SalesTax.validateTaxNumber(countryCode, vatNumber)
      .then(res => { return { isValid: res } })
  }

  static determineRate (salesTax) {
    return (salesTax.area === 'regional' || salesTax.area === 'national') ? salesTax.rate * 100 : 0.00
  }
}

module.exports = SalesTaxService

The crucial parts here are setting your origin country. This determines your base VAT rate (21% for the Netherlands). The rest are just a bunch of convenience functions because we also use this code to provide correct info to the front end. This done with two very simple RPC endpoints that directly call the service.

  getSalesTax (request, reply) {
    const { countryCode, vatNumber } = request.payload
    SalesTaxService.getSalesTax(countryCode, vatNumber)
      .then(tax => {
        reply(tax)
      })
      .catch(err => {
        reply.badImplementation(err)
      })
  }

  isValidVATNumber (request, reply) {
    const { countryCode, vatNumber } = request.payload
    SalesTaxService.validateVatNumber(countryCode, vatNumber)
      .then(res => {
        reply(res)
      })
      .catch(err => {
        reply.badImplementation(err)
      })
  }

On the Vue.js front end, this looks as follows. Notice that in the example I'm selling to a German customer that did not provide a valid VAT number. This means, as I'm in the Netherlands, I need to apply the German 19% tax rate.

The Vue.js code to do this is fairly simple. It's not more than two XHR calls to the endpoints shown above and some basic form blocking / updating depending on the state. Of course, malicious users could fool around with this. That is why we redo the whole thing on the backend.

Checkout form with VAT applied

The end result of all this is a correct calculation of VAT in your Stripe account. The below screenshot is test data for a Dutch customer that has 21% VAT applied.

Stripe invoice with 21% tax applied

Stripe is not accounting software

Lastly, for those new to Stripe, billing and payment stuff in general, remember that Stripe really is not accounting software. If you run a business in the EU (Netherlands in our case) remember that:

  • You may need to register for the VAT MOSS program if you have B2C customers.
  • The Stripe invoices are probably not correct EU VAT invoices.
  • Your payout from Stripe need to be accounted for an matched in some way to your invoices.

banner image: Utagawa Hiroshige (1797-1858), "Fleeting Pleasures", Japan,  source

checkly

Sign up for more SaaS stories. New content every Monday! 🍍

Show Comments