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: national registry fallback and enrichment
When the Italian VIES path is unavailable, or when you want more than a bare yes/no, a national tax authority or company registry can back-fill the answer. This is the same pattern vatnode runs across member states: try VIES first, and when it cannot answer — or when the national source carries richer company data — enrich the result from the national registry and record which source actually answered.
That is why the response carries a source field. Branch your invoicing logic on it rather than assuming every valid result came from the same place:
if (data.source === 'VIES') {
// Trader name/address came from VIES — use them on the invoice.
} else {
// A national registry answered — the field set may differ;
// reconcile against what you already hold, or ask the customer.
}
Do not hardcode a list of which countries have a fallback path — coverage changes, and the honest description is "tax authority and company registry APIs" rather than a fixed roster.
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(VIESor the national registry that answered).consultationNumberwhen the answer came from a requester-qualified VIES call — the VIES-issued reference tying the lookup to a requester on a date. It isnullfor national-source answers.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 why it is null on fallback paths — 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, national registry fallback and enrichment, and a stable response shape with a source you can branch on. 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",
// "companyForm": "S.r.l.",
// "source": "VIES",
// "consultationNumber": "WAPIAAAAX9999999",
// "checkId": "chk_...",
// "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. When the answer came from a national registry, consultationNumber is null and source names the database that answered — persist both exactly as returned. 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, falls back to national sources when VIES is down, and returns a stable response with the consultation number for your audit trail. Free plan, 100 requests/month.