Offline VAT number validation for the EU and a few other countries. It checks format and checksum, has no dependencies, and makes no network calls.
It's a drop-in replacement for jsvat, which was archived in 2026-03.
import { checkVAT } from "@codeshark/vat";
checkVAT("BE0411905847").isValid; // true
checkVAT("BE0411905848").isValid; // false, bad checksumA valid result means the number is well formed, not that it's registered. Use VIES for that.
- Fast. About 190 ns per call, 7× faster than jsvat 2.5.4 on valid numbers and 40× faster on
input that matches no country. Run
pnpm benchto measure it yourself. - Current rules. It follows recent national formats, such as Belgian numbers starting with 1 (issued since 2023), and checks Italian office codes and French letter keys. Coming from jsvat? See what changed.
- Tested against shared vectors. The test cases are plain JSON in
test-vectors/, so ports in other languages can reuse them.
npm install @codeshark/vatESM only, with TypeScript types included. CommonJS consumers on Node 20.19+ can
require("@codeshark/vat") directly.
Coming from jsvat? Change the import. Exports and the result shape match jsvat 2.5.4.
- import { checkVAT, countries } from "jsvat";
+ import { checkVAT, countries } from "@codeshark/vat";checkVAT(vat: string, countries?: Country[], options?: { assumeCountry?: Country }): VatCheckResultimport { checkVAT, belgium } from "@codeshark/vat";
checkVAT("BE0411905847"); // checks against all countries
checkVAT("BE0411905847", [belgium]); // only Belgium
checkVAT("0411905847", undefined, { assumeCountry: belgium }); // number has no prefixcountries defaults to all supported countries. assumeCountry applies only when the number
doesn't match a format as typed. It must be in countries, or checkVAT throws a TypeError.
interface VatCheckResult {
value?: string; // uppercased, with whitespace and . , / - removed
isValid: boolean; // format and checksum both pass
isValidFormat: boolean;
isSupportedCountry: boolean;
country?: {
name: string;
isoCode: { short: string; long: string; numeric: string };
};
}When the format matches, value is the official form:
| Country | Input | value |
|---|---|---|
| Belgium | BE411905847 |
BE0411905847 |
| Greece | EL94051189 |
EL094051189 |
| Netherlands | NL4495445B01 |
NL004495445B01 |
| Norway | NO974760673 |
NO974760673MVA |
| Switzerland | CHE272990183, CHE272990183TVA, ... |
CHE272990183MWST |
Andorra, Austria, Belgium, Brazil, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia,
Europe (EU), Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania,
Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania, Russia, Serbia, Slovakia,
Slovenia, Spain, Sweden, Switzerland, United Kingdom (GB, and XI for Northern Ireland).
Each country is exported by its camelCase name, such as belgium or unitedKingdom. Note
czechRepublic and slovakiaRepublic. countries holds them all.
EU numbers are issued to non-EU businesses under the non-Union OSS scheme. They have no public
checksum, so only the format (EU plus 9 digits) is checked.
Accepts numbers jsvat rejects:
- Belgian numbers starting with 1, issued since 2023 (
BE1006023325). - Greek and Dutch numbers missing leading zeros (
EL94051189,NL4495445B01). - Andorran numbers with any final control letter (
ADL708585T). - Digit-only Brazilian numbers. jsvat treats
02.061.065/0001-72as Andorran and rejects it. XI, as the United Kingdom.
Rejects numbers jsvat accepts:
- Polish numbers that can't have a valid check digit (
PL1234567890,PL0000000000). - Andorran
F,AandLnumbers outside their official ranges (ADA000000A). - French numbers with letter keys and a bad checksum (
FRXX433138302). - Italian numbers without a known office code: 001–100, 120, 121, 888 or 999 (jsvat allowed up to 201).
Other changes:
- The countries list is optional and defaults to all countries. jsvat reports every number as invalid without it.
valueis the official form for Belgium, Greece, the Netherlands, Norway and Switzerland.- Commas count as separators.
pnpm test: Vitest overtest-vectors/*.jsonpnpm typecheck:tsc, no emitpnpm build:tsc -p tsconfig.build.json→dist/(per-file ESM +.d.ts)pnpm lint/pnpm lint:fix: Biome (also runs on staged files via husky + lint-staged)pnpm bench: ns per call for a few input types
Each test-vectors/<country>.json holds valid, validOnlyByFormat and invalid lists,
converted from jsvat's test/countries_vat_lists.
MIT. Derived from jsvat 2.5.4 by Sergei Panfilov. See LICENSE and LICENSE.jsvat.