Community node that puts VIES VAT validation and EU rate lookups inside n8n, so an onboarding or invoicing workflow can decide on a real VAT number instead of
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.
Key Results

Every business selling across European markets has to charge the right VAT rate — and those rates differ by country and change over time. The rates themselves are public data the European Commission publishes for free, yet there is no reliable, free, ready-to-use source a developer can drop into a product.
So teams reach for bad options: hardcode the rates and quietly fall out of date, scrape a website that breaks, or pay a third-party API $50–200/month for numbers that are already public. Every one of those is either a compliance risk or an unnecessary cost.
eu-vat-rates-data is the open-source fix — free, correct VAT rates for every EU market plus the UK, updated daily from the official source, ready to use in whatever language a team already builds in. No registration, no API keys, no usage limits.
I built and maintain a free, open dataset of EU VAT rates and published it as a native package for the five most common backend languages — JavaScript/TypeScript, Python, PHP, Go, and Ruby — so any team can install it and get correct rates without changing their stack. It covers all 27 EU member states plus the UK, with every rate type each country uses.
A daily automated job pulls the latest rates straight from the European Commission's official database, so the data stays current with no one having to watch tax legislation. And because every change is committed to a public git history, the project doubles as an auditable record of when each rate changed — useful for anyone who needs to prove which rate applied on a given date.
All source code and data are publicly available at github.com/vatnode.
| 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) |
For the technically curious, here is how the dataset is sourced, shaped, and shipped.
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>"""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 being written.
SOAP namespace stripping. XML tags arrive with full namespaces: {urn:...}vatRateResults. Stripped explicitly: el.tag.split("}")[-1].
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.
All five packages expose the same logical API — getRate, getStandardRate, isEUMember, dataVersion — adapted to each language's idioms.
npm install eu-vat-rates-data · GitHubFull 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.
pip install eu-vat-rates-data · GitHubTypedDict 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.0composer require vatnode/eu-vat-rates-data · GitHubfinal 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'); // falsego get github.com/vatnode/eu-vat-rates-data-go · GitHubJSON 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") // falsegem install eu_vat_rates_data · GitHubModule 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") # => falseThe JS repository is the single source of truth. Its GitHub Actions workflow runs at 07:00 UTC daily:
eu-vat-rates-data.json (rates only, not version date)2026.M.D, with counter suffix if that version exists), rebuilds, publishes to npm, commits + pushesversion date in JSON but skips publish and taggingAll 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.
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
Community node that puts VIES VAT validation and EU rate lookups inside n8n, so an onboarding or invoicing workflow can decide on a real VAT number instead of
WordPress plugin that keeps WooCommerce EU tax rates current from the European Commission and applies the B2B reverse charge from a VIES-verified VAT number at
How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist automatically with GitHub Actions — architecture, versioning, and per-ecosystem
IndexNow implementation guide for Next.js: key generation, TypeScript client with retry logic, GitHub Actions workflow, and pitfalls that break submissions