Validitool

API documentation

Validate EU VAT and EORI numbers against the official European Commission registries, screen counterparties against the EU, UN, UK and US OFAC sanctions lists, check supplier email domains for invoice-fraud signals, and keep proof of every check as a PDF certificate.

Getting started

1. Sign in and generate an API key on your dashboard. The key starts with vt_live_ and is shown once, so store it somewhere safe.

2. Send it in the x-api-key header on every request. All endpoints live under https://validitool.com/api/v1, take and return JSON, and must be called from your server: never put the key in browser or mobile app code.

Machine-readable: the OpenAPI 3.1 spec imports straight into Postman, Insomnia or a code generator.

Your first request
curl -X POST https://validitool.com/api/v1/validate \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vatNumber":"IE6388047V"}'
Response
{
  "source": "live_vies",
  "valid": true,
  "countryCode": "IE",
  "vatNumber": "6388047V",
  "name": "GOOGLE IRELAND LIMITED",
  "address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
  "consultationNumber": null,
  "validatedAt": "2026-09-26T09:00:00.000Z",
  "stale": false,
  "certificateId": "3a7e9c10-2b4d-4f6e-8a1c-5d9b0e2f4a6c"
}

Errors and quotas

Errors come back with a non-2xx status and a JSON body with a human-readable error message you can show or log as is:

{
  "error": "Monthly rate limit of 50 requests exceeded for the 'free' tier."
}
400Bad request
Missing or malformed input. The message says which field.
401Unauthorized
Missing x-api-key header, or the key is invalid or revoked.
403Forbidden
The feature is not included in your plan.
404Not found
The job, certificate or item does not exist for this API key.
409Conflict
The resource is not ready yet (e.g. a batch job still processing).
429Quota exceeded
Your monthly request quota (or a plan limit, e.g. watched numbers) is used up. Retrying will not help until the next month or an upgrade.
503Registry unavailable
The EU registry (VIES or EORI) could not answer right now. Retry later; Scale plans can use allowStale instead.

What counts as a request: every check (VAT, EORI, compliance, email domain, IBAN, VAT rate, reverse charge) counts as one request, and every item in a bulk job counts as one when it is processed. Requests rejected before a check runs (bad key, plan not eligible, invalid JSON) do not count. Reading results, certificates, your watchlist, webhooks and account info is free, and so are the daily re-checks of monitored VAT numbers. Checks report your remaining quota in the X-RateLimit-Limit and X-RateLimit-Remaining response headers.

Plans and limits

FreeStarterGrowthScale
Requests per month502,00020,000100,000
Compliance checks—YesYesYes
PDF certificates1 per monthUnlimitedUnlimitedUnlimited
Items per bulk job——5005,000
Monitored VAT numbers—252502,500
Outage fallback (allowStale)———Yes

See pricing for prices. Quotas reset at the start of each calendar month.

Validate a VAT number

Checks an EU VAT number against the European Commission's VIES registry and returns the registered name and address where the member state provides them (some, such as Germany, do not). Results are cached for 24 hours.

POST/api/v1/validate

All plans · 1 request

vatNumberstring, required
With its country prefix, e.g. IE6388047V. Spaces and punctuation are ignored.
countryCodestring
Two-letter country code, if vatNumber has no prefix.
requesterCountryCodestring
Your own country code. With requesterVatNumber, some member states return a consultation number as official proof of the check.
requesterVatNumberstring
Your own VAT number (see above).
allowStaleboolean
Scale plan. If VIES is down, return the last successful result (up to 30 days old) instead of a 503, marked stale: true.
companyNamestring
The name you have for this company. The response then includes nameCheck (see Company name check).

source is live_vies, cache or stale_cache. certificateId downloads a PDF proof of the check (see Certificates). If VIES cannot answer for the member state right now, you get a 503 rather than a false "invalid".

Validate an EORI number

Checks an EORI (customs) number against the European Commission's EORI service, or HMRC's Check an EORI number service for UK numbers starting with GB (Northern Ireland's XI numbers are the EU's). Names and addresses are returned only where the trader has agreed to publish them.

POST/api/v1/validate-eori

All plans · 1 request

eoriNumberstring, required
The EORI number, 3–17 characters, e.g. DE123456789012345.
allowStaleboolean
Scale plan. Same outage fallback as for VAT numbers.
companyNamestring
The name you have for this trader; the response then includes nameCheck.
Request
curl -X POST https://validitool.com/api/v1/validate-eori \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eoriNumber":"IE6388047V"}'
Response
{
  "valid": true,
  "eoriNumber": "IE6388047V",
  "name": null,
  "address": null,
  "validatedAt": "2026-09-26T09:00:00.000Z",
  "stale": false,
  "certificateId": "8b1d4f2e-6c3a-4e9b-9f7d-2a5c8e1b3d6f"
}

Peppol e-invoicing lookup

Whether a company can receive e-invoices over the Peppol network, under which Peppol IDs, and in which formats. A VAT number is checked under every Peppol ID it maps to: the VAT-number scheme (for example 9925:BE…), and the national company number where it follows from the VAT number (Belgium 0208, Denmark 0184/0198, Sweden 0007, Finland 0216, Italy 0211). Registration comes from the Peppol network's own registry (SML), the formats from the company's service provider (SMP), and the name from the Peppol Directory when the company publishes one.

POST/api/v1/peppol-lookup

All plans · 1 request

vatNumberstring
With country prefix, e.g. BE0417497106.
countryCodestring
Two-letter country code, if vatNumber has no prefix.
participantIdstring
Instead of vatNumber: a full Peppol ID, e.g. 0208:0417497106 or a GLN (0088:…).
Request
curl -X POST https://validitool.com/api/v1/peppol-lookup \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vatNumber":"BE0417497106"}'
Response (abridged)
{
  "query": "BE0417497106",
  "registered": true,
  "canReceiveInvoices": true,
  "businessName": "AB InBev SA/NV International",
  "businessCountry": "BE",
  "participants": [
    {
      "participantId": "0208:0417497106",
      "scheme": "BE:EN",
      "schemeName": "Belgian enterprise number",
      "registered": true,
      "serviceProvider": "smp.tickstar.com",
      "canReceiveInvoices": true,
      "documentTypes": [
        {
          "id": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
          "name": "Peppol BIS Billing 3.0: Invoice (UBL)"
        },
        {
          "…": "(one entry per format)"
        }
      ]
    },
    {
      "participantId": "9925:BE0417497106",
      "…": "(same shape)"
    }
  ],
  "checkedAt": "2026-09-28T09:00:00.000Z"
}

registered: false means none of the IDs checked is on Peppol; the company may still be registered under another identifier (a GLN, a Dutch KvK number), which you can pass as participantId. A registered company whose service provider doesn't answer gets documentTypesError instead of documentTypes. If the Peppol registry itself can't be reached, the request fails with HTTP 503 rather than reporting "not registered". Compliance checks with a VAT number include the same result as peppol (informational: it never changes the verdict).

LEI lookup

Checks a Legal Entity Identifier against GLEIF, the global LEI register: the legal name, whether the entity still exists, whether its LEI is current, and its legal address. The two check digits are verified first, so a mistyped LEI fails with 400 without a lookup.

POST/api/v1/lei-lookup

All plans · 1 request

leistring, required
20 characters, e.g. 5493001KJTIIGC8Y1R12.
Request
curl -X POST https://validitool.com/api/v1/lei-lookup \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lei":"5493001KJTIIGC8Y1R12"}'
Response
{
  "lei": "5493001KJTIIGC8Y1R12",
  "found": true,
  "legalName": "Bloomberg Finance L.P.",
  "entityStatus": "ACTIVE",
  "registrationStatus": "ISSUED",
  "current": true,
  "jurisdiction": "US-DE",
  "legalAddress": "C/O Corporation Service Company, 251 Little Falls Drive, 19808 Wilmington, US",
  "headquartersAddress": "731 Lexington Avenue, 10022 New York, US",
  "registrationAuthorityId": "RA000602",
  "registeredAs": "4348344",
  "initialRegistrationDate": "2012-06-06T15:52:00Z",
  "lastUpdateDate": "2026-01-05T14:20:40Z",
  "nextRenewalDate": "2027-01-25T16:50:02Z",
  "checkedAt": "2026-09-28T09:00:00.000Z"
}

current is true when the entity is ACTIVE and its LEI is ISSUED (or pending transfer or archival). LAPSED means the LEI wasn't renewed on time, so its data hasn't been re-verified recently. An LEI that doesn't exist returns found: false.

Company register lookup

Looks a company up in its national register, for close trading partners outside the EU's VIES system: Switzerland (UID register, Federal Statistical Office) and Norway (Register of Legal Entities, Brønnøysund), and the UK (Companies House). Returns the official name, whether the company is active, dissolved or in insolvency, its legal form and address, and for Switzerland and Norway whether it's registered for VAT. Swiss and Norwegian check digits are verified first.

POST/api/v1/company-lookup

All plans · 1 request

countrystring, required
CH, NO or GB.
numberstring, required
Swiss UID or VAT number (CHE-105.909.036), Norwegian organisation or VAT number (923609016, NO923609016MVA), or UK company number (00445790).
Request
curl -X POST https://validitool.com/api/v1/company-lookup \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country":"CH","number":"CHE-101.654.423"}'
Response
{
  "country": "CH",
  "registry": "Swiss UID register (Federal Statistical Office)",
  "number": "CHE-101.654.423",
  "found": true,
  "name": "Swisscom (Schweiz) AG",
  "status": "active",
  "statusDetail": "Active (definitive)",
  "vatRegistered": true,
  "vatNumber": "CHE-101.654.423 MWST",
  "legalForm": "Company limited by shares (AG / SA)",
  "address": "Alte Tiefenaustrasse 6, 3050 Bern, CH",
  "registeredOn": null,
  "checkedAt": "2026-09-28T09:00:00.000Z",
  "certificateId": "7b2d4f10-9c3e-4a6b-8e1f-2d5c0a9b3e7f"
}

status is active, provisional, dissolved, insolvency (bankrupt, in liquidation or being wound up) or unknown; statusDetail is the register's own wording. A number the register doesn't know returns found: false. The Swiss register allows about 20 lookups a minute across all callers, so heavy use may briefly return 503.

Counterparty compliance check

One call to vet a new customer or supplier: validates their VAT and/or EORI number, screens the company name (and the registered name returned by VIES) against the EU, UN, UK and US OFAC sanctions lists, and optionally checks their email domain for fraud signals. You get a single clear / review_needed verdict with the reasons, and a PDF certificate for your records.

POST/api/v1/compliance-check

Starter, Growth, Scale · 1 request

vatNumberstring
With country prefix, e.g. DE811569869. Swiss (CHE-105.909.036 MWST) and Norwegian (NO923609016MVA) numbers are checked against their national registers.
countryCodestring
Two-letter country code, if vatNumber has no prefix.
eoriNumberstring
EORI number.
leistring
Legal Entity Identifier, checked with GLEIF. An unknown LEI or an entity that no longer exists makes the verdict review_needed; a lapsed LEI doesn't.
ukCompanyNumberstring
UK Companies House number. A dissolved or insolvent company makes the verdict review_needed.
companyNamestring
Recommended: some countries return no name from VIES, and without a name there is nothing to screen. It is also compared with the registered names (nameCheck); a clearly different name makes the verdict review_needed.
emailstring
A contact email address; its domain is checked for fraud signals.
knownDomainstring
The domain you already know this company uses (needs email). Catches lookalike domains.
allowStaleboolean
Scale plan. Outage fallback for the VAT/EORI part; a stale result always makes the verdict review_needed.

At least one of vatNumber, eoriNumber, companyName or email is required.

Request
curl -X POST https://validitool.com/api/v1/compliance-check \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vatNumber":"IE6388047V","companyName":"Google Ireland Limited","email":"billing@google.com","knownDomain":"google.com"}'
Response (abridged)
{
  "overallStatus": "clear",
  "reasons": [],
  "vat": {
    "checked": true,
    "valid": true,
    "number": "IE6388047V",
    "name": "GOOGLE IRELAND LIMITED",
    "address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4"
  },
  "eori": {
    "checked": false
  },
  "sanctions": {
    "screened": true,
    "namesScreened": [
      "Google Ireland Limited"
    ],
    "potentialMatches": [],
    "lists": [
      {
        "list": "eu",
        "name": "EU Consolidated Financial Sanctions List",
        "published": "2026-09-25",
        "syncedAt": "2026-09-26T04:05:00.000Z",
        "available": true
      },
      {
        "list": "un",
        "name": "UN Security Council Consolidated List",
        "published": "2026-09-25",
        "syncedAt": "2026-09-26T04:15:00.000Z",
        "available": true
      },
      {
        "list": "uk",
        "name": "UK Sanctions List",
        "published": "2026-09-25",
        "syncedAt": "2026-09-26T04:15:00.000Z",
        "available": true
      },
      {
        "list": "us_ofac",
        "name": "US OFAC Specially Designated Nationals (SDN) List",
        "published": "2026-09-24",
        "syncedAt": "2026-09-26T04:15:00.000Z",
        "available": true
      }
    ],
    "list": {
      "generationDate": "2026-09-25T18:01:34.799Z",
      "fileId": "185564",
      "syncedAt": "2026-09-26T04:05:00.000Z"
    }
  },
  "emailDomain": {
    "domain": "google.com",
    "risk": "low",
    "signals": [],
    "…": "(full email domain result, see below)"
  },
  "peppol": {
    "query": "IE6388047V",
    "registered": false,
    "canReceiveInvoices": false,
    "…": "(full Peppol result, see Peppol e-invoicing lookup)"
  },
  "checkedAt": "2026-09-26T09:00:00.000Z",
  "certificateId": "5c2e8a1f-9d3b-4f7a-8e6c-1b4d7f9a2c5e"
}

Company name check

With companyName, nameCheck.result says how it compares with the name the registry holds: match (the same name, ignoring case, accents, punctuation, legal forms such as GmbH or Ltd, and small typos), partial (one contains the other, typically a short or trading name), mismatch (nothing meaningful in common: a valid number registered to a different company is a common invoice-fraud set-up) or no_registered_name (the registry doesn't publish names, as with German and Spanish VAT numbers and most EORI records). registeredName and source say which name it was compared with.

nameCheck
{
  "result": "mismatch",
  "givenName": "Atlas Logistics GmbH",
  "registeredName": "NORDIC TIMBER AB",
  "source": "VAT register"
}

Reasons for review_needed

vat_invalid
VAT number is not valid according to VIES
vat_check_failed
VAT number could not be checked (registry unavailable)
eori_invalid
EORI number is not valid according to the EU EORI registry
eori_check_failed
EORI number could not be checked (registry unavailable)
vat_result_stale
VIES was unavailable; the VAT result is the last successful check, not a live one
eori_result_stale
The EORI registry was unavailable; the EORI result is the last successful check, not a live one
sanctions_potential_match
Potential match on a sanctions list
no_name_to_screen
No company name was available to screen against the sanctions lists
sanctions_list_unavailable
Sanctions screening could not be completed because a list was unavailable
sanctions_list_stale
A sanctions list has not been refreshed in over 72 hours
email_domain_high_risk
The email domain shows high-risk fraud signals
lei_not_current
The LEI was not found, or its entity is no longer active
lei_check_failed
The LEI could not be checked (GLEIF register unavailable)
uk_company_not_active
The UK company is dissolved, in insolvency, or not found at Companies House
uk_company_check_failed
The UK company could not be checked (Companies House unavailable)
name_mismatch
The company name given is different from the name the registers hold for these numbers

A potentialMatches entry means a name is similar to a listed person or entity, with the list it's on (list: eu, un, uk or us_ofac), that list's own reference, the programme and a 0–1 similarity score. The same party often appears on several lists, so expect one entry per list. It is a prompt for a human to review, not a determination. Each list is downloaded daily from its official publisher; lists shows each one's edition, and if any list couldn't be loaded the verdict is review_needed with sanctions_list_unavailable. list (singular) is the EU list's edition, kept for existing integrations.

Email domain fraud check

Checks the domain behind an email address for signs of invoice or payment-diversion fraud: domains that don't exist or can't receive mail, newly registered domains, throwaway and free mailbox providers, domains anyone can spoof, and lookalikes of a supplier domain you already know (suppIier.com, supplier-invoices.com, supp1ier.com).

POST/api/v1/email-domain-check

All plans · 1 request

emailstring
The email address to check. Either email or domain is required.
domainstring
A bare domain, if you have no address.
knownDomainstring
The supplier's real domain, to detect lookalikes.
Request
curl -X POST https://validitool.com/api/v1/email-domain-check \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"billing@suppIier-invoices.com","knownDomain":"supplier.com"}'
Response
{
  "email": "billing@suppIier-invoices.com",
  "domain": "suppiier-invoices.com",
  "registrableDomain": "suppiier-invoices.com",
  "exists": false,
  "mx": {
    "acceptsMail": false,
    "hosts": []
  },
  "spf": {
    "present": false,
    "allPolicy": null,
    "record": null
  },
  "dmarc": {
    "present": false,
    "policy": null,
    "pct": null,
    "record": null
  },
  "spoofable": null,
  "freeProvider": false,
  "disposable": false,
  "domainAge": {
    "registeredAt": null,
    "days": null
  },
  "knownDomain": {
    "domain": "supplier.com",
    "match": "lookalike"
  },
  "signals": [
    {
      "code": "domain_not_found",
      "severity": "high",
      "message": "The domain suppiier-invoices.com does not exist."
    },
    {
      "code": "known_domain_lookalike",
      "severity": "high",
      "message": "suppiier-invoices.com looks very similar to the known domain supplier.com but is not the same domain: a common way to impersonate a supplier."
    }
  ],
  "risk": "high",
  "checkedAt": "2026-09-26T09:00:00.000Z"
}

risk is the highest signal severity (low when there are none). knownDomain.match is one of exact, subdomain, same_name_different_tld, lookalike, contains_known_name, free_provider or different. Domain age comes from the registry's RDAP service; some registries (e.g. .de, .it) don't publish it, so days can be null.

Signals

domain_not_foundhigh
The domain does not exist.
domain_cannot_receive_emailhigh
No mail server: the address cannot receive email.
domain_rejects_emailhigh
The domain declares that it receives no email.
disposable_emailhigh
A throwaway email service.
domain_very_newhigh
Registered in the last 30 days.
known_domain_lookalikehigh
Imitates the known domain.
known_domain_different_tldhigh
Same name as the known domain, different ending.
known_domain_name_reusedhigh
Contains the known domain name but is a different domain.
known_domain_free_providerhigh
Free mailbox instead of the known company domain.
domain_newmedium
Registered in the last 180 days.
free_email_providermedium
Gmail, Outlook, GMX etc. rather than a company domain.
no_mx_recordmedium
No MX record (mail may still be delivered to the domain directly).
spf_allows_anyonemedium
SPF authorises every server to send as this domain.
internationalized_domainmedium
Non-Latin characters that can imitate a familiar domain.
known_domain_mismatchmedium
Unrelated to the known domain.
no_dmarc / dmarc_not_enforced / dmarc_partiallow
Forged email from this domain may be delivered.
no_spf / spf_invalidlow
No valid list of servers allowed to send for this domain.

Inside a compliance check, only a high risk domain turns the verdict into review_needed.

Validate an IBAN

Checks an IBAN's structure and checksum (ISO 13616) and splits out the bank identifier. It does not confirm that the account exists or who owns it.

POST/api/v1/validate-iban

All plans · 1 request

ibanstring, required
The IBAN; spaces are ignored.
Request
curl -X POST https://validitool.com/api/v1/validate-iban \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"iban":"DE89 3704 0044 0532 0130 00"}'
Response
{
  "valid": true,
  "iban": "DE89370400440532013000",
  "countryCode": "DE",
  "bankIdentifier": "37040044",
  "accountNumber": "0532013000"
}

VAT rates

Standard and reduced VAT rates for every EU member state (national rates from the European Commission's TEDB database) and for European countries outside the EU: the UK (GB), Switzerland, Liechtenstein, Norway, Iceland, Türkiye, Ukraine, Moldova, Andorra and the Western Balkans. This is reference data, not a live registry: check lastReviewed and verify against the official rates before invoicing.

POST/api/v1/vat-rate

All plans · 1 request

countryCodestring, required
Two-letter country code, e.g. DE or CH (GR and UK are accepted for EL and GB).
Request
curl -X POST https://validitool.com/api/v1/vat-rate \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"countryCode":"CH"}'
Response (disclaimer shortened)
{
  "countryCode": "CH",
  "country": "Switzerland",
  "standardRate": 8.1,
  "reducedRates": [
    2.6,
    3.8
  ],
  "inEuVatArea": false,
  "taxName": "VAT",
  "note": "3.8% is the special rate for accommodation.",
  "lastReviewed": "2026-09-28",
  "disclaimer": "Reference data, not a live registry lookup like VIES. …"
}

inEuVatArea is false for countries outside the EU VAT area; taxName is the local name where it isn't VAT (IGI in Andorra, MVA in Norway); note covers rates that don't fit the two columns, such as zero rates or sector-specific rates.

Reverse charge

Tells you whether the B2B reverse-charge mechanism applies to a sale between EU countries, or between the UK and the EU, checking the buyer's VAT number as part of it. This is guidance based on the general EU and UK rules, not tax advice. Use GB for Great Britain and XI for Northern Ireland, which follows EU rules for goods and UK rules for services. Goods between Great Britain and the EU are exports and imports, so they return not_applicable with the export and import VAT treatment explained.

POST/api/v1/reverse-charge

All plans · 1 request

transactionTypestring, required
goods or services.
sellerCountryCodestring, required
Your country code.
buyerCountryCodestring, required
The buyer's country code.
buyerVatNumberstring, required
The buyer's VAT number.
serviceCategorystring
For services: general (default), real_estate, admission_events, restaurant_catering, short_term_vehicle_hire or other_exception. The exceptions have special rules and return consult_advisor.
buyerIsBusinessboolean
Recommended. false (a private buyer, even with a VAT number) returns not_applicable.
fixedEstablishmentboolean
Services. Either party has a branch in the other's country: true returns consult_advisor.
goodsTransportedboolean
Goods. false (goods don't leave the country) returns consult_advisor.
thirdPartyInvolvedboolean
Goods. A chain or triangular sale: true returns consult_advisor.
installedOrAssembledboolean
Goods. Installed or assembled on site: true returns consult_advisor.
deliveredDutyPaidboolean
Goods between Great Britain and the EU. The seller handles the import (DDP): true returns consult_advisor.
Request
curl -X POST https://validitool.com/api/v1/reverse-charge \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transactionType":"services","sellerCountryCode":"DE","buyerCountryCode":"IE","buyerVatNumber":"IE6388047V"}'
Response (disclaimer shortened)
{
  "determination": "reverse_charge_applies",
  "reason": "Cross-border B2B service under the general rule. The seller charges no VAT; the buyer self-accounts for it in their own country.",
  "legalBasis": "EU VAT Directive, Article 44",
  "buyerVatValid": true,
  "invoiceNote": "Reverse charge: VAT to be accounted for by the recipient (Article 196 of Directive 2006/112/EC)",
  "assumptions": [
    "The buyer is buying for their business.",
    "Neither party has a branch or fixed establishment in the other party's country."
  ],
  "vatToCharge": {
    "basis": "reverse_charge",
    "ratePercent": 0,
    "country": null,
    "note": "No VAT on the invoice: the customer accounts for the VAT in their own country."
  },
  "disclaimer": "This is guidance based on the general EU and UK cross-border VAT rules, not tax or legal advice. …",
  "certificateId": "9d4e2b71-3a5c-4f8e-b6d0-1c7a9e3f5b28"
}

determination is reverse_charge_applies, not_applicable (domestic sale, an export or import of goods, or the buyer's VAT number is not valid) or consult_advisor. invoiceNote is suggested wording for the invoice where there's a standard one, otherwise null. buyerVatValid is null when the number wasn't or couldn't be checked. The yes/no questions are optional for backwards compatibility, but send them: each one you leave out is assumed to be the simple case and listed in assumptions, so an empty assumptions means the answer rests only on what you confirmed.

vatToCharge says what goes on the invoice, where the case is clear-cut. basis is standard_rate (a domestic sale: ratePercent is that country's standard rate; reduced or zero rates apply to some goods and services, so check the rate for what you sell), zero_rated (an intra-Community supply or an export: 0%), reverse_charge (0%: the customer accounts for the VAT) or depends (a consumer sale, where the rate depends on facts such as your yearly sales: ratePercent is null and note explains). It is null when the answer is consult_advisor. Each answer has a certificateId for its PDF record.

Certificates

VAT, EORI, company register, reverse-charge and compliance checks return a certificateId. Download the PDF certificate (who was checked, when, against which registry and with what result) to keep as audit evidence. Certificates are only available to the API key that ran the check.

GET/api/v1/certificate/{certificateId}

All plans · doesn't use requests; Free plan limited to 1 download per month

Request
curl https://validitool.com/api/v1/certificate/3a7e9c10-2b4d-4f6e-8a1c-5d9b0e2f4a6c \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -o certificate.pdf

Bulk validation

Submit up to 500 (Growth) or 5,000 (Scale) VAT or EORI numbers at once. The job runs in the background: poll its status, or subscribe to the batch_job.completed webhook. The whole batch must fit in your remaining monthly quota when you submit it.

No code needed for a one-off list: on Growth and Scale, upload a CSV at Dashboard → Bulk check and download the results as a spreadsheet.

POST/api/v1/batch/validate

Growth, Scale · 1 request per item, counted as processed

Request
curl -X POST https://validitool.com/api/v1/batch/validate \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"countryCode":"IE","vatNumber":"6388047V"},{"countryCode":"DE","vatNumber":"811569869"}]}'
Response (201)
{
  "jobId": "f0c1e2d3-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "status": "pending",
  "totalItems": 2
}
POST/api/v1/batch/validate-eori

Growth, Scale · 1 request per item, counted as processed

Request body
{
  "items": [
    {
      "eoriNumber": "IE6388047V"
    },
    {
      "eoriNumber": "DE123456789012345"
    }
  ]
}
GET/api/v1/batch/{jobId}

Growth, Scale · doesn't use requests

status is pending until every item is processed, then completed.

Response
{
  "jobId": "f0c1e2d3-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "checkType": "vat",
  "status": "pending",
  "totalItems": 2,
  "processedItems": 1,
  "createdAt": "2026-09-26T09:00:00.000Z",
  "completedAt": null
}
GET/api/v1/batch/{jobId}/results

Growth, Scale · doesn't use requests

Returns 409 until the job has completed. Each result's status is done or error (with the reason in error, e.g. the registry was unavailable).

Response
{
  "jobId": "f0c1e2d3-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "checkType": "vat",
  "totalItems": 2,
  "results": [
    {
      "value": "IE6388047V",
      "status": "done",
      "valid": true,
      "companyName": "GOOGLE IRELAND LIMITED",
      "companyAddress": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
      "error": null
    },
    {
      "value": "DE811569869",
      "status": "done",
      "valid": true,
      "companyName": null,
      "companyAddress": null,
      "error": null
    }
  ]
}

VAT number monitoring

Add your customers' VAT numbers to a watchlist and Validitool re-checks them about once a day. When one becomes invalid (or valid again) you get an email (set up in your dashboard, on by default) and a vat_number.status_changed webhook. Each company is also screened against the EU, UN, UK and US OFAC sanctions lists, under the companyName you give and the registered name, again whenever a list is updated; a new potential match is emailed to you. Re-checks don't use your quota.

POST/api/v1/watchlist

Starter: 25, Growth: 250, Scale: 2,500 numbers · free

Request
curl -X POST https://validitool.com/api/v1/watchlist \
  -H "x-api-key: $VALIDITOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"countryCode":"IE","vatNumber":"6388047V","companyName":"Google Ireland Limited"}'

companyName is optional but recommended: some registries (Germany's and Spain's) return no name, and without one there is nothing to screen.

GET/api/v1/watchlist

All plans · doesn't use requests

Returns { "watchlist": [...] }; each entry has id, full_vat_number, company_name, last_known_valid, last_known_name, last_checked_at, and the latest sanctions screening: sanctions_matches (potential matches, each with list, reference, matched_name, input_name and a 0–1 score; null until first screened), sanctions_names and sanctions_screened_at.

DELETE/api/v1/watchlist/{id}

All plans · doesn't use requests

Webhooks

Validitool sends events to your HTTPS endpoint as a JSON POST. There are two ways to receive them: set one webhook URL on your dashboard, or subscribe URLs per event through the API (up to 50).

POST/api/v1/webhooks

All plans · doesn't use requests

targetUrlstring, required
Your https:// endpoint.
eventstring, required
vat_number.status_changed or batch_job.completed.
sourcestring
Optional label for your own reference (max 30 characters).
Response (201)
{
  "id": "9e8d7c6b-5a49-4382-9170-6f5e4d3c2b1a",
  "event": "vat_number.status_changed",
  "targetUrl": "https://example.com/hooks/validitool",
  "source": null,
  "createdAt": "2026-09-26T09:00:00.000Z",
  "secret": "<signing secret, shown only in this response>"
}
GET/api/v1/webhooks

All plans · doesn't use requests

DELETE/api/v1/webhooks/{id}

All plans · doesn't use requests

If your endpoint answers 410 Gone, the subscription is removed automatically. Delivery is attempted once, with a 10-second timeout.

Event payloads

vat_number.status_changed
{
  "event": "vat_number.status_changed",
  "vatNumber": "DE811569869",
  "previousStatus": "valid",
  "currentStatus": "invalid",
  "companyName": null,
  "companyAddress": null,
  "checkedAt": "2026-09-26T06:00:00.000Z"
}
batch_job.completed
{
  "event": "batch_job.completed",
  "jobId": "f0c1e2d3-4b5a-6978-8a9b-0c1d2e3f4a5b",
  "totalItems": 2,
  "completedAt": "2026-09-26T09:03:00.000Z"
}

Verifying the signature

Every delivery carries an X-Validitool-Signature: sha256=… header: an HMAC-SHA256 of the raw request body, keyed with your signing secret (from the subscription response, or your dashboard for the dashboard webhook). Reject requests where it doesn't match.

Node.js
import { createHmac, timingSafeEqual } from 'crypto';

// rawBody: the exact request body as a string, before JSON.parse
function isFromValiditool(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  return (
    typeof signatureHeader === 'string' &&
    signatureHeader.length === expected.length &&
    timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
  );
}

Account

Your plan, quota usage and features. Useful for testing a key.

GET/api/v1/me

All plans · doesn't use requests

Request
curl https://validitool.com/api/v1/me \
  -H "x-api-key: $VALIDITOOL_API_KEY"
Response
{
  "tier": "growth",
  "monthlyLimit": 20000,
  "usedThisMonth": 1234,
  "features": {
    "complianceCheck": true,
    "allowStale": false,
    "batchLimit": 500,
    "watchlistLimit": 250
  }
}

Questions or something missing? Email support@validitool.com.