npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vatverify/node

v0.2.1

Published

Official Node.js client for the vatverify API. Validate VAT numbers, run tax-rules decisions, fetch rates. Cross-runtime: Node 18+, Bun, Deno, Vercel Edge, Cloudflare Workers.

Readme

@vatverify/node

npm types license downloads

Official TypeScript + Node.js SDK for the vatverify VAT validation API. VIES goes down on Tuesdays, HMRC rate-limits, and the Swiss UID register speaks SOAP. One typed client handles all three.

  • 🇪🇺 EU-27 via VIES
  • 🇬🇧 UK via HMRC
  • 🇨🇭 Switzerland / Liechtenstein via BFS (UID register)
  • 🇳🇴 Norway via Brønnøysundregistrene
  • 🇩🇪 Germany via BZSt eVatR — §18e qualified confirmation (client.confirm())
  • Freshness-aware responses (live / cached / degraded) so a registry outage never 502s your checkout
  • /decide tax-rules engine for reverse-charge invoice decisions
  • Runs on Node.js 22+, Bun, Deno, Vercel Edge, and Cloudflare Workers, with zero runtime dependencies
npm install @vatverify/node

Supported registries

| Country | Registry | Transport | |---|---|---| | EU-27 | VIES | SOAP | | XI (Northern Ireland) | VIES | SOAP | | UK (GB) | HMRC | REST | | CH / LI | BFS (Swiss UID) | SOAP | | NO | Brønnøysundregistrene | REST | | DE (§18e qualified confirmation) | BZSt eVatR | REST |

Live rolling 30-day uptime and p50/p95 latency per registry: vatverify.dev/status. All numbers come from the public GET /v1/status.json endpoint, with no made-up SLAs.

Northern Ireland VATs use the XI prefix under the Brexit protocol; they validate through VIES like any EU member. GB numbers route to HMRC.

Quick start

import { Vatverify } from '@vatverify/node';

const client = new Vatverify('vtv_live_...');

const { data, meta } = await client.validate({ vat_number: 'IE6388047V' });
console.log(data.valid, data.company?.name);
console.log('latency:', meta.latency_ms, 'ms');
console.log('freshness:', meta.source_status); // 'live' | 'cached' | 'degraded'

Get an API key at vatverify.dev. Free tier: 500 live validations / month plus unlimited test-mode calls, no credit card.

Validation

client.validate(input)

Validate a single VAT number. Available on every plan.

const { data, meta } = await client.validate({
  vat_number: 'IE6388047V',
  cache: false,                           // optional: bypass the 30-day cache
  requester_vat_number: 'DE100000001',    // optional: consultation number / audit trail
});

client.validateBatch(input)

Validate up to 50 VAT numbers in one request. Requires the Pro or Business plan.

const { data, meta } = await client.validateBatch({
  vat_numbers: ['IE6388047V', 'DE811569869', 'FR44732829320'],
});
console.log(`${data.summary.successful}/${data.summary.total} succeeded`);
for (const item of data.results) {
  if (item.ok) console.log(item.data.vat_number, '→', item.data.valid);
  else console.log('error:', item.error.code, item.error.message);
}

Tax decisions: /decide

The differentiator. Answers "should I charge VAT on this invoice, or is it reverse-charge / out-of-scope?" and returns the legal basis plus the exact invoice_note string to print on the invoice. Requires the Business plan.

// DE seller → FR B2B buyer: intra-EU reverse charge
const { data } = await client.decide({
  seller_vat: 'DE123456789',
  buyer_vat: 'FR44732829320',
});
data.mechanism;    // 'reverse_charge'
data.invoice_note; // 'Reverse charge: VAT to be accounted for by the recipient (Art. 196 VAT Directive).'
data.legal_basis;  // 'EU Directive 2006/112/EC, Art. 196'
// DE seller → DE B2B buyer: domestic, standard VAT
await client.decide({ seller_vat: 'DE123456789', buyer_vat: 'DE811569869' });
// → { mechanism: 'standard', rate: 19, ... }
// DE seller → non-EU buyer: out of scope
await client.decide({ seller_vat: 'DE123456789', buyer_country: 'US' });
// → { mechanism: 'out_of_scope', invoice_note: '...' }

mechanism is one of 'standard', 'reverse_charge', 'zero_rated', 'out_of_scope'. Both VATs are validated against their live registries in the same call, so you get validation + decision for one quota unit, pooled with /validate.

BZSt §18e qualified confirmation

German sellers shipping inside the EU need a qualifizierte Bestätigungsmitteilung from the Bundeszentralamt für Steuern (BZSt) as legal evidence under §18e UStG. The SDK calls BZSt's eVatR endpoint, returns a per-field A/B/C/D match grid (name / street / postcode / town), and stores the result for 10 years per German law. Business plan only.

const { data, meta } = await client.confirm({
  vat_number: 'NL007051104B01',
  company: {
    name: 'ASML Netherlands B.V.',
    street: 'De Run 6501',
    postcode: '5504DR',
    town: 'Veldhoven',
  },
});

data.qualified;          // true only if every supplied field returned 'A'
data.matches.name;       // 'A' | 'B' | 'C' | 'D'
data.confirmation_id;    // your evidence id — retrievable for 10 years
meta.bzst_status_code;   // 'evatr-0000' on full match
meta.bzst_id;            // BZSt's own request identifier (independent evidence)

Retrieve a stored confirmation later:

const { data } = await client.confirmations.get(confirmationId);

Match codes: A matches, B does not match, C not requested, D not provided by the foreign EU registry. The requester VAT (a German VAT-IdNr.) defaults to the one stored on the API key; override per-call with requester_vat_number. Pass an idempotency_key UUID to safely retry on network flakes — replays return the same confirmation_id for 24h.

Audit log

Every /validate, /validate_batch, and /decide response is persisted by request_id. Retrieve the exact response envelope later for tax-audit evidence or dispute resolution.

const { data } = await client.audits.get('019dbc5f-6191-77f6-b3c1-ed4ba503bc44');
data.endpoint;   // 'validate' | 'decide' | 'validate_batch'
data.response;   // original response body
data.created_at; // ISO timestamp
data.expires_at; // retention cutoff

Retention: 7 days (Starter), 30 days (Pro), 90 days (Business). Free plan keys return 404. Full docs: vatverify.dev/docs/api/audit/get-audit-log.

Rates

const { data } = await client.rates.list();
console.log(`${data.length} countries`);

const { data: de } = await client.rates.get('de');
console.log(de.standard_rate, de.currency);  // 19 EUR

Rates endpoints are public, no auth required. For fully offline rates plus format/checksum validation (no API call at all), use @vatverify/vat-rates instead.

Reliability

Every registry fails in its own way. The SDK and API surface what's happening so your code can respond:

  • meta.source_status: 'live': fresh response from the registry.
  • meta.source_status: 'cached': served from the 30-day cache (within freshness window).
  • meta.source_status: 'degraded': the registry failed live, so the response was served from the fallback cache window. The VAT is still validated, but treat the answer as "last known good" rather than real-time.

This means a VIES outage doesn't break your checkout: the request returns a degraded response instead of a 502. The public status page (vatverify.dev/status) shows the live state of every registry.

Retries

The SDK retries on network errors, timeouts, 429, 502, 503, and 504: up to 2 retries (3 total attempts), exponential backoff with jitter, capped at 2s. Retry-After on 429 takes precedence (capped at 30s). Retries never fire on 400, 401, 402, 404, which are caller errors.

// disable retries globally or per request
const client = new Vatverify({ api_key: '...', max_retries: 0 });
await client.validate(input, {
  request_options: { max_retries: 0, timeout: 5000, signal: controller.signal },
});

Test mode

Test keys (vtv_test_...) exercise the full API deterministically without consuming quota or hitting registries:

const client = new Vatverify('vtv_test_...');

await client.validate({ vat_number: 'IE6388047V' });    // valid, Apple Distribution International Ltd
await client.validate({ vat_number: 'DE811569869' });   // valid, Zalando SE
await client.validate({ vat_number: 'FR44732829320' }); // valid, BlaBlaCar SAS
await client.validate({ vat_number: 'IE0000000X' });    // invalid, no company data
await client.validate({ vat_number: 'DE999999999' });   // 502 registry_unavailable
// any other well-formed VAT → valid, synthesized "Magic Corp (XX)"

Full fixture list at vatverify.dev/docs/test-mode. Test-mode calls are unlimited on every plan including free.

Configuration

// string shorthand (common case)
const client = new Vatverify('vtv_live_...');

// or full config
const client = new Vatverify({
  api_key: 'vtv_live_...',
  base_url: 'https://api.vatverify.dev', // default
  timeout: 30_000,                        // default: 30s per attempt
});

// or VATVERIFY_API_KEY env var
const client = new Vatverify();

Advanced options (max_retries, fetch, user_agent_extra, on_response hook) are documented at vatverify.dev/docs/sdks/node.

Error handling

import {
  VatverifyError,
  AuthError, ValidationError, NotFoundError, PlanError,
  RateLimitError, RegistryError, NetworkError, TimeoutError,
} from '@vatverify/node';

try {
  await client.validate({ vat_number: 'xxx' });
} catch (e) {
  if (e instanceof RateLimitError) {
    console.log(`Retry after ${e.retry_after}s; remaining: ${e.rate_limit.remaining}`);
  } else if (e instanceof RegistryError) {
    console.log('Upstream registry failed; response was served degraded or unavailable');
  } else if (e instanceof AuthError) {
    console.log('Rotate the API key');
  } else if (e instanceof VatverifyError) {
    console.log(e.code, e.status_code, e.request_id, e.attempt_count);
  }
}

Every error exposes code, status_code, request_id (quote this in support tickets), response_body, and attempt_count. RateLimitError additionally carries retry_after and rate_limit: { limit, remaining, reset }.

Runtime support

| Runtime | Supported | |---|---| | Node.js 22+ | ✅ | | Bun | ✅ | | Deno | ✅ | | Vercel Edge | ✅ | | Cloudflare Workers | ✅ | | Browsers (direct API key) | ❌ (API keys must stay server-side) |

Zero runtime dependencies. Uses only fetch, AbortController, URL, Headers.

TypeScript

Types ship with the package and are auto-generated from the production OpenAPI spec. Every method is fully typed end-to-end:

import type { ValidateResponse, CountryRate, BatchResultItem, DecideResponse } from '@vatverify/node';

License

MIT. See LICENSE.