eu-vat-rates-data — Free & Open-Source EU VAT Rates Dataset

February 25, 2026

Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems. Auto-updated daily from the official European Commission TEDB API — no API keys, no subscriptions, no paywalls.

Tech Stack

Data Source

EC TEDB SOAP APIXML/SOAP parsingPython requests

JavaScript / TypeScript

TypeScripttsup (CJS + ESM)npm

Other Languages

Python 3 (PyPI)PHP (Packagist)Go (pkg.go.dev)Ruby (RubyGems)

Automation

GitHub ActionsCalendar versioning (2026.M.D)Cross-repo sync

Key Results

  • Free forever — no API keys, no rate limits, no paywalls
  • 5 package ecosystems: npm, PyPI, Packagist, Go, RubyGems
  • 28 countries — EU-27 + UK, 4 rate types per country
  • Daily auto-update from official EC TEDB SOAP API
  • Git history = auditable record of every EU VAT rate change
npmPyPIPackagistGoRubyGemsLicenseUpdated

Why This Exists

Every developer building for European markets eventually hits the same wall: EU VAT rates are public data, but there's no reliable, free, machine-readable source.

The common solutions are bad: hardcode the rates (and miss updates), scrape a website (brittle), or pay $50–200/month for a third-party API (vatstack, vatlayer) to access data that the European Commission publishes for free.

eu-vat-rates-data is the open-source fix. Free data, available as native packages for every major language, updated daily from the official source. No registration, no API keys, no usage limits. Just install and use.

All source code and data are publicly available at github.com/vatnode.

Data Source

The dataset is sourced from the European Commission TEDB (Taxes in Europe Database) — the official EU SOAP web service at ec.europa.eu/taxation_customs/tedb/ws/.

Each day, a Python script sends a typed XML request for all 28 countries with situationOn set to today's date, parses the structured response, and writes the result to data/eu-vat-rates-data.json:

soap_body = f"""<v1:retrieveVatRatesReqMsg>
  <types:memberStates>
    {''.join(f'<types:isoCode>{c}</types:isoCode>' for c in COUNTRY_CODES)}
  </types:memberStates>
  <types:situationOn>{today}</types:situationOn>
</v1:retrieveVatRatesReqMsg>"""

Non-obvious edge cases

EL → GR normalization. TEDB uses the EU convention EL for Greece instead of the ISO 3166-1 standard GR. Explicit mapping: TEDB_TO_ISO = {"EL": "GR"}.

UK hardcoded fallback. After Brexit, the UK was removed from TEDB. GB rates (20% standard, 5% reduced) are stored as a static fallback and updated manually when legislation changes.

Non-numeric rate filtering. TEDB returns not just percentages but also EXEMPTED, OUT_OF_SCOPE, NOT_APPLICABLE. All non-numeric values are filtered out — only positive floats make it into the dataset.

Deduplication. Some countries (France, Portugal, Spain) have territorial special rates that appear as duplicate entries in the SOAP response. Rates are aggregated into a set() before writing.

SOAP namespace stripping. XML tags arrive with full namespaces: {urn:...}vatRateResults. Stripped explicitly: el.tag.split("}")[-1].

Dataset Structure

28 countries, 4 rate types, 8 non-EUR currencies tracked:

{
  "version": "2026-02-25",
  "source": "European Commission TEDB",
  "rates": {
    "FI": {
      "country": "Finland",
      "currency": "EUR",
      "standard": 25.5,
      "reduced": [10.0, 13.5],
      "super_reduced": null,
      "parking": null
    },
    "LU": {
      "country": "Luxembourg",
      "currency": "EUR",
      "standard": 17.0,
      "reduced": [8.0, 14.0],
      "super_reduced": 3.0,
      "parking": 14.0
    }
  }
}

Historical rate changes are not stored in the file — they're preserved automatically in git history. git log -- data/eu-vat-rates-data.json gives a complete audit trail of every EU VAT rate change since the project launched.

5 Packages, 5 Ecosystems

All five packages expose the same logical API — getRate, getStandardRate, isEUMember, dataVersion — adapted to each language's idioms.

JavaScript / TypeScript — npm install eu-vat-rates-data · GitHub

Full TypeScript types. CountryCode is a union literal of all 28 codes. getRate is overloaded: pass a CountryCode and get VatRate (never undefined); pass a plain string and get VatRate | undefined. isEUMember is a type guard that narrows string to CountryCode.

import { getRate, isEUMember, dataVersion } from "eu-vat-rates-data";

if (isEUMember(userInput)) {
  const rate = getRate(userInput); // VatRate — never undefined here
  console.log(`${rate.country}: ${rate.standard}%`);
}
console.log(dataVersion); // "2026-02-25"

Published as dual CJS + ESM with .d.ts declarations.

Python — pip install eu-vat-rates-data · GitHub

TypedDict with Optional annotations. Data loaded via importlib.resources.files() — the modern Python approach that works correctly inside wheels and zip archives.

from eu_vat_rates_data import get_rate, is_eu_member

rate = get_rate("FI")
# VatRate(country='Finland', currency='EUR', standard=25.5, ...)

if is_eu_member("DE"):
    print(get_rate("DE")["standard"])  # 19.0

PHP — composer require vatnode/eu-vat-rates-data · GitHub

final class EuVatRates with all-static methods and lazy loading — JSON is read once on first access. PHPDoc array shape annotations for IDE support.

use VATNode\EuVatRates\EuVatRates;

$rate = EuVatRates::getRate('FI');       // ['standard' => 25.5, 'reduced' => [...]]
EuVatRates::getStandardRate('DE');       // 19.0
EuVatRates::isEuMember('US');           // false

Go — go get github.com/vatnode/eu-vat-rates-data-go · GitHub

JSON embedded via //go:embed — the binary contains the dataset, no runtime file I/O. Nullable fields use pointer types (*float64). Parsing runs in init().

import euvatrates "github.com/vatnode/eu-vat-rates-data-go"

rate, ok := euvatrates.GetRate("FI")
// rate.Standard == 25.5, rate.Country == "Finland"

standard, _ := euvatrates.GetStandardRate("DE") // 19.0
euvatrates.IsEUMember("US")                      // false

Ruby — gem install eu_vat_rates_data · GitHub

Module EuVatRatesData with lazy memoization (@dataset ||= JSON.parse(...)). Safe navigation operator for nullable fields.

require "eu_vat_rates_data"

rate = EuVatRatesData.get_rate("FI")
# => {"country"=>"Finland", "standard"=>25.5, "reduced"=>[10.0, 13.5], ...}

EuVatRatesData.get_standard_rate("DE") # => 19.0
EuVatRatesData.eu_member?("US")        # => false

Automation Architecture

The JS repository is the single source of truth. Its GitHub Actions workflow runs at 07:00 UTC daily:

  1. Fetches rates from EC TEDB SOAP API
  2. Compares with existing eu-vat-rates-data.json (rates only, not version date)
  3. If rates changed: bumps version (2026.M.D, with counter suffix if that version exists), rebuilds, publishes to npm, commits + pushes
  4. If unchanged: updates version date in JSON but skips publish and tagging

All other language repos (Python, PHP, Go, Ruby) run at 08:00 UTC — one hour later — and pull the JSON directly from the JS repo via curl. This guarantees the source file is already committed before dependent workflows start.

Results

MetricValue
Packages published5 (npm, PyPI, Packagist, Go, RubyGems)
Countries covered28 (EU-27 + UK)
Rate types4 (standard, reduced, super_reduced, parking)
Update frequencyDaily (automated)
Manual interventionZero (unless EC TEDB changes its API)
Iurii RoguliaAvailable

Need something similar?

I build custom solutions — from APIs to full products. Let's talk about your project.

View all projects

Related projects