The zero-rate check: the VAT number you validated once, two years ago
Your customer's VAT number is checked before every zero-rated invoice, and again each quarter. The proof is filed with the sale, not remembered.
Every EU business customer's VAT number checked before the invoice leaves and again each quarter, with the EU's own proof code filed next to the sale
- Your own VAT number - without it in the request the EU returns no consultation number, and you have a check you cannot prove you made
- A list of which customers you actually invoice without VAT as a business in another EU member state - usually 5 to 50 rows, not thousands
- An always-on machine or a mini-VPS at EUR 5-12/month running Docker
- Your accountant told, once, that this ledger exists - they own the rest of the exemption file
Version history
Every EU business customer's VAT number checked before the invoice leaves, and again every quarter, with the EU's own reference code filed next to the sale.
The architecture
the same flow as text
▼
[ n8n (self-hosted) — one HTTP call ]
│ GET ec.europa.eu/taxation_customs/vies/rest-api/ms/<MS>/vat/<number>
│ ?requesterMemberStateCode=<yours>&requesterNumber=<yours>
▼
[ The check ledger (Grist) — one row per check, never overwritten ]
├─ valid / invalid, date, name and address as returned
├─ requestIdentifier — the EU's own consultation number
└─ the raw JSON, stored as-is
valid → the customer stays zero-rateable
invalid → flag on the customer, mail to you, nothing sent to the customer by machine
service down → retry, never recorded as "invalid"
Three triggers, one call, one append-only ledger. The design decision is the ledger: you are not storing "this customer is valid", you are storing "on this date the EU answered this". A status field can be overwritten. A dated row with a consultation number is evidence.
The problem
You sell to a company in another EU country. They give you their VAT number, you leave the VAT off the invoice, everyone is happy. Three years later an inspector opens that file and asks one question: was that number valid on the day you invoiced, and can you show it. Since 1 January 2020 this stopped being paperwork. Directive (EU) 2018/1910 — the third of the "quick fixes" — made a valid VAT identification number, communicated by the customer, a substantive condition for zero-rating an intra-Community supply, not a formality you can repair afterwards. If the number was cancelled, or was never valid, the exemption is exposed and the VAT is yours to pay, on a sale whose margin is long gone. Most small businesses check the number once, at onboarding, on the EU website, and never again. Companies deregister, go bankrupt, restructure, change legal form. The number rots quietly and nothing tells you.
Tool choices — and why
GET /taxation_customs/vies/rest-api/ms/BE/vat/0417497106 returns JSON, no API key, no registration, no cost. Checked live on 19/09/2026: it answers isValid, requestDate, name, address. Add requesterMemberStateCode and requesterNumber — your own VAT number — and the response fills requestIdentifier with a consultation number (format WAPIAAAA…). Without those two parameters that field comes back empty. That string is the point of the whole exercise: it is the Commission's own proof that you asked, on that date. Every paid VAT-validation API in this space is a wrapper around the same free endpoint.Setup outline
- Pull your customer list. Keep only the ones you invoice without VAT because they are a business in another EU member state. For most small businesses that is 5 to 50 rows, not thousands.
- Build the ledger table: customer, country code, VAT number, checked_at, is_valid, returned_name, returned_address, consultation_number, raw_response, trigger (onboarding / pre-invoice / quarterly). Append-only — no row is ever updated.
- In n8n, one HTTP Request node to the REST endpoint, with your own VAT number in the two requester parameters. Test it against a number you know is good and one you know is bad before anything else is wired.
- Branch on three outcomes, not two: valid, invalid, and unreachable. Unreachable retries after an hour and alerts you if it is still unreachable after three tries.
- Trigger 1 — onboarding: a form or a webhook when you add an EU business customer. No valid check, no zero-rated invoice.
- Trigger 2 — pre-invoice: if your invoicing tool has a webhook, check the number when the invoice is created. Cheap, and it catches the deregistration that happened last month.
- Trigger 3 — quarterly sweep, spread over the night: re-check every active EU business customer before you file the EC Sales List for that quarter.
- One page for your accountant: customer, last check date, result, consultation number. That page is what gets attached to the file when someone asks.
Pitfalls — what goes wrong when you build this
MS_UNAVAILABLE when a member state does not answer. Code that treats a timeout as a failed check will, on a bad Tuesday, flag half your customers as dead. Retry with a delay and record the attempt as inconclusive.--- where the name and address should be. Never build a rule that requires the returned name to match your customer record — you will block real customers in those countries. Use the name when it comes, log it when it doesn't.requesterMemberStateCode and requesterNumber, requestIdentifier is an empty string and you have a check you cannot prove you made. This is the single most common way this gets built wrong.Verified repos
n8n — Sustainable Use License (free self-hosted internal use), 204k stars, v2.39.5 released 14/09/2026
Grist — Apache-2.0, active, v1.7.19
NocoDB — Sustainable Use License, release 2026.09.0 (alternative to Grist)
VIES — European Commission, free REST API, no key, verified live 19/09/2026