Verified n8n node that puts VIES VAT validation and EU rate lookups inside a workflow, so onboarding or invoicing can decide on a real VAT number instead of a
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.
Verified n8n node that puts VIES VAT validation and EU rate lookups inside a workflow, so onboarding or invoicing can decide on a real VAT number instead of a
Zero-dependency Halloween screen-corner decorations and ambient page effects — one script tag or npm install, class-driven, seasonal by default, under 10 KB
How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist automatically with GitHub Actions — architecture, versioning, and per-ecosystem
halloween.js adds Halloween decorations to any website — one class flips them on, a real-calendar season window turns them off automatically, and a