eu-vat-rates-data — Free & Open-Source EU VAT Rates Dataset
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
JavaScript / TypeScript
Other Languages
Automation
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
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:
- Fetches rates from EC TEDB SOAP API
- Compares with existing
eu-vat-rates-data.json(rates only, not version date) - If rates changed: bumps version (
2026.M.D, with counter suffix if that version exists), rebuilds, publishes to npm, commits + pushes - If unchanged: updates
versiondate 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
| Metric | Value |
|---|---|
| Packages published | 5 (npm, PyPI, Packagist, Go, RubyGems) |
| Countries covered | 28 (EU-27 + UK) |
| Rate types | 4 (standard, reduced, super_reduced, parking) |
| Update frequency | Daily (automated) |
| Manual intervention | Zero (unless EC TEDB changes its API) |
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.