Validate Italian VAT Numbers (Partita IVA) in Node.js
5 August 2026

Validate Italian VAT Numbers (Partita IVA) in Node.js
Italy is one of the larger B2B markets you will hit if you sell across the EU, and the Italian VAT ID has a quirk worth knowing before you write any validation code: it is all digits, with no letters to lean on, and it is easy to confuse with the other Italian tax code that looks similar. The format check is cheap. The part that actually matters – is this number registered and active – is a network call. Here is the full pipeline in Node.js.
What an Italian VAT number looks like
The Italian VAT ID is the Partita IVA. Its format is the prefix IT followed by 11 digits, all numeric – no letters anywhere in the body. The structure breaks down like this:
- Digits 1–7 – the taxpayer identifier.
- Digits 8–10 – the code of the provincial tax office (Agenzia delle Entrate) that issued it.
- Digit 11 – a check digit computed from the first ten.
The trap: Italy also issues a codice fiscale, a personal or entity tax code that, for individuals, is a 16-character alphanumeric string, and for companies is a 11-digit number that often equals the Partita IVA – but not always. The codice fiscale is not the VAT ID. Only the Partita IVA is what you validate against VIES for VAT purposes. If a customer sends you a 16-character code with letters in it, that is a codice fiscale for a natural person, and it is the wrong thing for a VAT check. Ask for the Partita IVA.
Step 1: format validation
Before any network call, reject input that cannot possibly be a Partita IVA. Two things happen here: normalization (uppercase, strip spaces and separators) and a shape check (regex). Keep this stage clearly labelled as shape-only – passing it means the string could be a Partita IVA, nothing more.
const IT_VAT_PATTERN = /^IT\d{11}$/
// Shape-only: proves the string is well-formed, NOT that it is registered.
function normaliseItalianVatId(input: string): string | null {
const cleaned = input.replace(/[\s.\-]/g, '').toUpperCase()
return IT_VAT_PATTERN.test(cleaned) ? cleaned : null
}
normaliseItalianVatId('IT 123 4567 8901') // → 'IT12345678901'
normaliseItalianVatId('12345678901') // → null (missing IT prefix)
normaliseItalianVatId('IT1234567890') // → null (only 10 digits)
Two decisions worth making explicitly:
- Prefix handling. Italians often write the Partita IVA as bare 11 digits without the
ITprefix, because domestically the prefix is not used. In a strict API you should require the fully-qualifiedIT…form and reject the rest. In a lenient checkout form you can prependITwhen the input is exactly 11 digits – just log the original input either way. - Do not reuse the 11-digit rule elsewhere. The
IT+ 11-digit shape is specific to Italy. Other member states have their own lengths and alphabets; do not copy this regex into a generic validator.
The Partita IVA checksum (Luhn-style): a soft signal, never gate on it
The eleventh digit of a Partita IVA is a check digit computed with a Luhn-style checksum (a modified Luhn over the first ten digits). Because it is a pure arithmetic property of the number itself, you can verify it offline with no network call, which makes it a cheap first filter for catching transposed or mistyped digits.
That is the only thing it is good for. A checksum-valid Partita IVA can be unassigned, never issued, or deregistered – the arithmetic says nothing about whether the number belongs to a real, currently-registered business. Treat a checksum pass as a soft signal and a checksum failure as a hint that the customer fat-fingered something, never as an authoritative yes/no.
If you want the offline filter, a compact implementation looks roughly like this – but read the caveat under it:
// Soft signal only. A pass does NOT mean the number is registered.
// This is one common Luhn-style variant; treat the result as advisory.
function looksLikeValidChecksum(elevenDigits: string): boolean {
if (!/^\d{11}$/.test(elevenDigits)) return false
let sum = 0
for (let i = 0; i < 10; i++) {
let d = elevenDigits.charCodeAt(i) - 48
if (i % 2 === 1) {
d *= 2
if (d > 9) d -= 9
}
sum += d
}
const check = (10 - (sum % 10)) % 10
return check === elevenDigits.charCodeAt(10) - 48
}
Do not treat that function as the canonical validator – it is one way to express the modified-Luhn rule, and you should never gate registration decisions on it. The registration answer comes from VIES.
Step 2: VIES and the IT node
VIES is the EU-wide validation service, and it is the only source that answers the question you actually care about: is this Partita IVA registered for intra-EU trade right now. VIES is a federation of national nodes – when you query an IT number, the European Commission routes the request to the Italian tax administration’s database and returns whatever it says. You can read the Commission’s own description on the VIES service pages.
For a live IT check you get back one of:
- A valid/invalid answer, with trader name and address when the number is valid and the trader has not opted out of disclosure.
- A ‘member state unavailable’ condition when the Italian node is temporarily down.
- A ‘service unavailable’ condition when VIES itself is degraded.
- A timeout – VIES is not fast, and ~10 seconds is a realistic upper bound.
The important mental model: format and checksum tell you the number is well-formed; only VIES tells you it is registered. Those are two different questions, and shipping only the first one is the most common mistake in home-grown validators.
Step 3: why there is no Italian fallback when VIES is down
For some member states, when the VIES path is unavailable you can back-fill the answer from a national tax authority that exposes the intra-EU VAT register itself – Poland’s Ministry of Finance white list and Romania’s ANAF both do. Italy is not one of them, and it is worth being precise about why, because Italy does have a public lookup and it is easy to mistake it for a fallback.
Italy does have a free public source for checking a Partita IVA – the Agenzia delle Entrate’s Verifica partita IVA service, available both as a web form and through AdE’s API Management platform. The catch is what it answers. It confirms that the number exists and is active in the Anagrafe Tributaria, attributed to a named taxpayer, domestically. It does not tell you whether the holder is authorized for intra-Community transactions, because in Italy that authorization is a separate, opt-in registration – the one that puts a business into the VIES archive in the first place. An Italian business can therefore hold a perfectly valid, active Partita IVA and still be absent from VIES. Reading the domestic answer as evidence of the intra-EU one is exactly the false positive you cannot afford during an outage.
The practical consequence is blunt: for an Italian Partita IVA, VIES is the only source of a yes/no. When VIES IT is down, the honest result is ‘unavailable’ – not ‘invalid’, and not a verdict fabricated from a domestic lookup. A resilient Italian validator does not reach for a second source; it treats the outage as transient, refuses to invent a verdict, and queues a re-check.
That is also why the response carries a source field, and why for Italian numbers it always reads VIES. Branch on the verdict and the error code rather than on which database answered:
if (data.valid) {
// VIES confirmed the number — use data.companyName / data.companyAddress
// on the invoice and store the consultationNumber.
} else if (data.error?.code === 'VIES_UNAVAILABLE') {
// The IT node is down. There is no Italian validity fallback — do NOT proceed
// as if the number were valid. Queue an asynchronous re-check.
}
Concretely: Italy is VIES-only in vatnode today. There is no national tax-authority or company-registry integration wired into the fallback or enrichment path for IT numbers, so an Italian result carries the VIES name and address and no company-registry data. The registryCode it returns is the Partita IVA itself, derived arithmetically from the number you passed in – not a Registro Imprese lookup, and not evidence of anything VIES did not already tell you. Do not hardcode a list of which countries have a fallback path, though – coverage changes, and the honest description is ‘tax authority and company registry APIs’ rather than a fixed roster. The current per-country position is published at /docs/coverage.
Step 4: handling errors
A resilient validator treats ‘we could not get an answer’ as a distinct state from ‘invalid’. Blocking checkout because VIES timed out is a self-inflicted wound. The vatnode API returns explicit, machine-readable error codes so you can branch correctly:
INVALID_FORMAT(400) – the string is not a well-formed VAT ID. This is a client mistake; surface it inline.INVALID_REQUESTER(422) – the requester VAT ID used for the qualified lookup was rejected.RATE_LIMITED(429) – back off and retry.VIES_UNAVAILABLE(503) – the VIES node is down. Transient; queue a retry, do not treat as invalid.VIES_ERROR(502) – VIES answered with an error. Transient.UPSTREAM_TIMEOUT(504) – the upstream took too long. Transient.INTERNAL_ERROR(500) – retry, then alert.
The rule of thumb: INVALID_FORMAT is the caller’s problem, everything in the 5xx family is a transient upstream problem you retry, and none of them mean ‘this VAT number is fake.’ For the fuller treatment of each state and retry strategy, see handle VIES error codes.
Step 5: storing the result as audit evidence
If you sell into Italy under the intra-EU reverse charge, you need to be able to show you validated the buyer’s VAT registration at the time of supply. That means persisting each check as an immutable row, not overwriting a customer’s VAT status in place.
Store, at minimum:
- The cleaned VAT ID (
IT+ 11 digits, uppercase, no separators). valid(boolean) andsource(VIES– the only authority for anITnumber).consultationNumberwhen the answer came from a requester-qualified VIES call – the VIES-issued reference tying the lookup to a requester on a date. It isnullwhen the request carried no requester.verifiedAt— the timestamp of the check.- The returned company fields (
companyName,companyAddress, and so on) you relied on for the invoice.
Append a new row per check; never mutate an old one, because the consultation number is timestamp-specific evidence. The reasoning behind treating it as your primary audit artefact – and when it comes back null – is covered in store the consultation number.
Full example with the vatnode API
vatnode runs the whole pipeline above on every request: format normalization, a requester-qualified VIES call to the IT node, and a stable response shape with explicit error codes to branch on when the node cannot answer. One call:
const res = await fetch('https://api.vatnode.dev/v1/vat/IT12345678901', {
headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` },
})
if (!res.ok) {
// Branch on the error code — see the errors table above.
const { code } = await res.json()
throw new Error(`VAT check failed: ${code}`)
}
const data = await res.json()
// {
// "valid": true,
// "vatId": "IT12345678901",
// "countryCode": "IT",
// "countryName": "Italy",
// "companyName": "Esempio S.r.l.",
// "companyAddress": "Via Roma 1, 20121 Milano MI",
// "source": "VIES",
// "consultationNumber": "WAPIAAAAX9999999",
// "checkId": "019d2a89-a5d9-7b97-b710-57b84604de2b",
// "verifiedAt": "2026-08-14T08:30:00.000Z"
// }
valid: true with source: "VIES" and a populated consultationNumber is the strong case: registered per VIES, with a reference you can keep. For Italian numbers source is always VIES, since nothing else can decide validity – persist it exactly as returned rather than assuming it.
Field names and every response attribute are in the API reference, and the Italian VAT validation endpoint documents the IT-specific behavior. Prefer a language-agnostic walkthrough? The Node.js VAT validation guide covers the storage schema, and the EU VAT API covers the rest of the member states.
FAQ
Is the Partita IVA the same as the codice fiscale?
No. The codice fiscale is a personal or entity tax code; the Partita IVA is the VAT number. Only the Partita IVA (prefixed IT) is what you validate against VIES for VAT purposes.
What is the format of an Italian VAT number?
An Italian VAT number is the prefix IT followed by 11 digits. The last digit is a Luhn-style check digit, but a correct format and checksum only prove the number is well-formed – not that it is registered.
Should I validate the checksum myself?
You can, as a cheap first filter to reject typos, but treat it as a soft signal only. The authoritative answer – whether the number is registered – comes from a live VIES check, not from the checksum.
Validate Partita IVA numbers without the VIES boilerplate
vatnode normalizes the input, runs a requester-qualified VIES call to the IT node, and returns a stable response with the consultation number for your audit trail – and when the IT node is down it surfaces a transient error instead of guessing. Free plan, 100 requests/month.