# Validate EU VAT numbers in Node, Python and PHP

Source: https://veriport.io/guides/validate-eu-vat-number-node-python-php (published 2026-10-24)

To validate an EU VAT number from Node.js, Python or PHP, send one GET to `/api/v1/vat/validate` with the number, read `valid`, and read `source.kind` to know whether VIES answered live or a dated cached answer was served. The three functions below do exactly that with each language’s standard HTTP client (fetch, requests, curl) and branch on the stable `code` of an error. No SDK to install: the API is plain HTTPS and JSON, described by an OpenAPI 3.1 document.

- The first 20 calls a day need no key; set `VERIPORT_KEY` once you have one.
- Pass `requester` (your own VAT number) to obtain a consultation number on the paid tiers.
- Treat `vies_unavailable` as "retry later", never as "invalid".
- Log `source.kind`, `source.checkedAt` and `consultationNumber` with the order: that is your audit evidence.

## Node.js

Native `fetch` (Node 18+), no dependency.

```javascript
const API = "https://veriport.io/api/v1";
const KEY = process.env.VERIPORT_KEY; // optional for the first 20 calls a day

export async function validateVat(country, number, requester) {
  const url = new URL(API + "/vat/validate");
  url.searchParams.set("country", country);
  url.searchParams.set("number", number);
  if (requester) url.searchParams.set("requester", requester);
  const res = await fetch(url, { headers: KEY ? { Authorization: "Bearer " + KEY } : {} });
  const body = await res.json();
  if (!res.ok) {
    // RFC 9457 problem: body.code is stable, body.detail is for humans
    if (body.code === "vies_unavailable") throw new Error("VIES down, retry in " + body.retryAfter + "s");
    throw new Error(body.code + ": " + body.detail);
  }
  // body.valid, body.name, body.address, body.consultationNumber
  // body.source.kind is "live" or "cache"; body.source.stale tells you which
  return body;
}

const r = await validateVat("DE", "136695976");
console.log(r.valid, r.source.kind, r.name);
```

## Python

`requests`; the standard library’s `urllib` works the same way.

```python
import os
import requests

API = "https://veriport.io/api/v1"
KEY = os.environ.get("VERIPORT_KEY")  # optional for the first 20 calls a day

def validate_vat(country: str, number: str, requester: str | None = None) -> dict:
    params = {"country": country, "number": number}
    if requester:
        params["requester"] = requester
    headers = {"Authorization": f"Bearer {KEY}"} if KEY else {}
    r = requests.get(f"{API}/vat/validate", params=params, headers=headers, timeout=15)
    body = r.json()
    if not r.ok:
        # RFC 9457 problem: body["code"] is stable, body["detail"] is for humans
        if body.get("code") == "vies_unavailable":
            raise RuntimeError(f"VIES down, retry in {body.get('retryAfter')}s")
        raise RuntimeError(f"{body.get('code')}: {body.get('detail')}")
    return body  # valid, name, address, consultationNumber, source.kind, source.stale

r = validate_vat("DE", "136695976")
print(r["valid"], r["source"]["kind"], r["name"])
```

## PHP

The `curl` extension; Guzzle users replace the transport and keep the branches.

```php
<?php
const API = "https://veriport.io/api/v1";
$key = getenv("VERIPORT_KEY") ?: null; // optional for the first 20 calls a day

function validateVat(string $country, string $number, ?string $requester = null, ?string $key = null): array {
    $query = http_build_query(array_filter(["country" => $country, "number" => $number, "requester" => $requester]));
    $ch = curl_init(API . "/vat/validate?" . $query);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
        CURLOPT_HTTPHEADER => $key ? ["Authorization: Bearer $key"] : [],
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $body = json_decode($raw, true);
    if ($status >= 400) {
        // RFC 9457 problem: $body["code"] is stable, $body["detail"] is for humans
        if (($body["code"] ?? "") === "vies_unavailable") {
            throw new RuntimeException("VIES down, retry in " . ($body["retryAfter"] ?? 30) . "s");
        }
        throw new RuntimeException(($body["code"] ?? "error") . ": " . ($body["detail"] ?? ""));
    }
    return $body; // valid, name, address, consultationNumber, source.kind, source.stale
}

$r = validateVat("DE", "136695976", null, $key);
var_dump($r["valid"], $r["source"]["kind"], $r["name"]);
```

## What should you store?

At minimum: the normalised number (`full`), `valid`, `requestDate`, `source.kind`, `source.checkedAt` and `consultationNumber`. For an invoice, print the VAT number and keep the rest with the order. Re-check numbers you invoice recurrently every quarter; deregistrations happen.

## IBAN in the same style

The IBAN endpoint never fails for a bad IBAN: it answers 200 with `valid: false` and the list of failed checks, so a form can be specific.

```javascript
const res = await fetch("https://veriport.io/api/v1/iban/validate?iban=" + encodeURIComponent(iban));
const r = await res.json();
if (!r.valid) {
  // r.failures: ["length"] | ["check_digits"] | ["bban_pattern"] | ["country_unknown"] | ["characters"]
  showError("IBAN rejected: " + r.failures.join(", "));
} else if (!r.sepa) {
  showWarning(r.countryName + " is outside SEPA: expect correspondent-bank fees and delays");
}
```

## Questions

### Is there an official SDK?

Yes: `@veriport/sdk` for TypeScript (generated from /openapi.json, with MIT offline helpers for the VAT number structure and the IBAN mod-97 check) and `veriport` for Python (httpx, sync and async), plus an MCP server for agents, all MIT in the public SDK repository linked from the About page; the npm and PyPI releases are announced in the changelog. The functions above need no SDK: the API is plain HTTPS and JSON, and the OpenAPI document feeds any generator (openapi-generator, orval, Kiota).

### Can I call it from the browser?

Technically yes (CORS is open), but a key in browser code is public. Use the anonymous allowance for demos and call from your server in production.

## Read next

- [Reference: EU VAT number validation](https://veriport.io/docs/vat-validation)
- [VIES is down: validate anyway](https://veriport.io/guides/vies-down-validate-eu-vat-number-anyway)
- [openapi.json](https://veriport.io/openapi.json)

## Sources

- VIES REST API — check-vat-number, check-status (technical information) — https://ec.europa.eu/taxation_customs/vies/#/technical-information (2026-10-24)
