Official Python SDK for VerifyVAT
Project description
VerifyVAT Python SDK
This is the official Python SDK for the VerifyVAT API. It provides a safe, convenient, and consistent way to call the API and to reason about results.
By using this SDK, you get:
- Safe API calls:
Authentication, retries with backoff, envelope validation, and a single error type with useful context for debugging. - Structured results:
Rich, typed results that include what sources said, coverage levels, issues, and more, so you can understand the full context of a verification. - Reasoning helpers:
Opinionated helpers to turn complex verification results into simple decision signals, as well as pickers to select the best name, address, or identifier for an entity based on your needs.
If you haven't already, start with VerifyVAT's documentation to understand the API's concepts and features, then come back here for SDK-specific usage and examples.
Installation
pip install verifyvat-sdk
Python requirement: ≥ 3.13.
Quickstart
from verifyvat_sdk import VerifyVatClient, Verifier
client = VerifyVatClient()
verifier = Verifier(client)
verification = verifier.verify_id(id="09446231", country="GB")
print(verification)
Environment variables are read automatically if set:
VERIFYVAT_API_KEY=your_api_key_here
# Optional for custom deployments:
# VERIFYVAT_BASE_URL=https://api.verifyvat.com/v1
FP helpers vs OO wrappers
The SDK exports both FP helpers (functions) and OO wrappers (classes) for core features. Both call the same underlying client and return the same results, the difference is in ergonomics and style.
FP helpers
When you want a simple, direct way to call the API without needing to manage instances of classes, the FP helpers are a great choice. They are straightforward functions that take the client as the first argument, followed by the parameters for the specific operation.
from verifyvat_sdk import verify_id, infer_id_type
v = verify_id(client, id="914778271", type="no_orgnr")
t = infer_id_type(client, id="914778271", country="NO")
OO wrappers
If you prefer an object-oriented style, or if you want to group related operations together, the OO wrappers provide a convenient way to do that. They are classes that you instantiate with the client once, and then call methods on the instance for specific operations.
from verifyvat_sdk import Verifier, TypeInferrer
verifier = Verifier(client, suppress_errors=True)
v = verifier.verify_id(id="914778271", type="no_orgnr")
inferrer = TypeInferrer(client, suppress_errors=True)
t = inferrer.infer_id_type(id="914778271", country="NO")
The client
The client is created via VerifyVatClient(...). It exists to make the boring but critical parts consistent.
The VerifyVatClient handles authentication via API key and standard headers, validation of the API envelope, timeouts, network failures, and retries with backoff, including rate-limit handling. It throws a single error type VerifyVatError with useful context for debugging.
We recommend creating the client once and reusing it across your app. It's designed to be lightweight and efficient.
import os
from verifyvat_sdk import VerifyVatClient, RetryPolicy
# While all options are optional, `api_key` is required if not set as an env var.
client = VerifyVatClient(
# Explicitly pass you api key and base URL, or set them as env vars
api_key=os.environ["VERIFYVAT_API_KEY"],
# base_url='https://api.verifyvat.com/v1',
# How long to wait for a response before giving up
timeout_ms=10_000,
# If you want to use a custom HTTP client implementation (e.g. in a non-standard environment)
# http=custom_http_client,
# Custom logger for retry attempts, errors, etc.
logger=lambda event: ..., # event: VerifyVatLogEvent
# Retry policy configuration
retry_policy=RetryPolicy(
max_retries=3, # max number of retries before giving up
backoff_base_ms=1_000, # initial backoff delay
backoff_max_ms=30_000, # max backoff delay
),
)
If you need to attach headers, pass request options at call time:
from verifyvat_sdk import verify_id
v = verify_id(
client,
id="09446231",
country="GB",
headers={"x-correlation-id": "abc"},
)
# To cancel the request:
# Use your runtime's cancellation mechanism; the SDK uses a synchronous HTTP client.
To learn more about the common response format from VerifyVAT, see Response envelope.
Verify an ID
Verification answers: "what did sources return, and how complete/reliable was it?"
from verifyvat_sdk import Verifier
verifier = Verifier(client)
verification = verifier.verify_id(id="09446231", country="GB")
print(verification)
You typically use verify_id in two modes:
1) Plain mode (throws on request failure)
This is the standard way to call the API. If the request fails (network error, non-2xx status, or an "unsuccessful" envelope), it throws a VerifyVatError with details about what went wrong.
v = verifier.verify_id(id="09446231", country="GB")
2) Pipeline mode (suppress_errors: True)
This is what you want in bulk/batch work, where "error as data" is better than throwing. With suppress_errors=True, the helper returns a tagged fallback verification object even when the request fails.
v = verifier.verify_id(id="09446231", country="GB", suppress_errors=True)
For more on error handling strategies and request failures, see Error handling.
Bulk verification (concurrency + per-item outputs)
Use verify_many_ids when you have a small batch of inputs to verify at once.
from verifyvat_sdk import Verifier
verifier = Verifier(client)
results = verifier.verify_many_ids(
items=[
{"id": "09446231", "country": "GB"},
{"id": "914778271", "type": "no_orgnr"},
"5493000IBP32UQZ0KL24",
],
# These are the default settings for bulk operations
concurrency=5,
suppress_errors=True,
)
for r in results:
print(r["input"], r["verification"])
This gives you:
- the normalized input the SDK actually processed (e.g. with inferred type)
- the verification result for each input, including in-band errors when
suppress_errorsis true
verify_many_ids supports both list and dict/record inputs. With dict inputs, the output is a dict with the same keys, which can be more convenient for lookups.
results = verifier.verify_many_ids(
items={
"a": {"id": "09446231", "country": "GB"},
"b": {"id": "914778271", "type": "no_orgnr"},
"c": "5493000IBP32UQZ0KL24",
},
concurrency=5,
suppress_errors=True,
)
print(results["a"]["input"], results["a"]["verification"])
For more on bulk verification, see Bulk verification.
describe_verification: turn results into signals
A verification result is a rich object with many fields about what sources said, coverage, issues, etc.
The describe_verification helper turns this into a consistent set of decision signals you can use in your product logic, without having to write custom rules for every source and edge case.
from verifyvat_sdk import Verifier
verifier = Verifier(client)
verification = verifier.verify_id(id="09446231", country="GB")
d = verifier.describe_verification(verification)
if d.retryRecommended:
# Re-add to the verification queue for a retry later
...
elif d.reviewRecommended:
# Flag as edge case for manual review
...
elif d.isConfirmed:
# Proceed with confidence
...
else:
# Reject or block
...
Policy overrides
There might be cases where you want to override the default classification logic based on your product's needs or risk appetite. For example, you might want to never trust cache data, or to flag certain syntax issues for review.
You can do this by passing a policy function to describe_verification that tweaks the decision signals based on the verification context.
def custom_policy(ctx):
if ctx.origin == "fresh-cache":
# We decide to never accept data from cache, even if it's "fresh"
return {"isConfirmed": False, "retryRecommended": True}
elif "syntax-invalid" in ctx.issues:
# We decide to review any identifiers with invalid syntax
return {"reviewRecommended": True}
else:
return None # no override
d2 = verifier.describe_verification(
verification,
policy=custom_policy,
)
Per-source overrides
You can override per-source classification if you intend to reason about specific sources in your product logic. For example, you could decide to accept stale cache data only if it comes from a specific source.
d3 = verifier.describe_verification(
verification,
source_policy=lambda source_ctx: (
{"has_stale_outcome": False} if source_ctx.source.id == "xx-reg" else None
),
)
any_stale = any(s.has_stale_outcome for s in d3.sources)
For more practical examples of how to use describe_verification and how to think about decision signals, see Verify & decide.
If you want to understand what stale and fresh data means and how to reason about it, see Caching and stale data.
Infer ID type
Use this when a user pastes "something" and you don't yet know what type of identifier it is (VAT number, company number, LEI, etc.). The SDK returns ranked candidates with confidence scores, and a helper to pick the best one when confidence is high.
To learn more about how inference works and how to use it in your product, see Infer ID type.
from verifyvat_sdk import infer_id_type, pick_best_inferred_id_type
result = infer_id_type(client, id="914778271", region="EMEA")
inferred_type = pick_best_inferred_id_type(
result,
confidence={"min": 0.8},
)
if not inferred_type:
# ask for country or specific ID type
pass
else:
print(
f"Most likely type: {inferred_type.type} in {inferred_type.details.country}, with confidence {inferred_type.confidence}"
)
OO wrapper version:
from verifyvat_sdk import TypeInferrer
inferrer = TypeInferrer(client)
result = inferrer.infer_id_type(id="914778271", region="EMEA")
DomainGraph: ID types, sources, coverage
DomainGraph is a read-only model built from the API's ID type and data source definitions. You can use it to understand what types and sources are available, what coverage they have, and to make decisions like "can we verify this ID without calling the API?" or "should we show this source's data on the UI?".
See List supported IDs and List data sources for more on the underlying API endpoints.
Fetch once, use everywhere
You can instantiate a DomainGraph and reuse it across your app.
It has built-in caching and refresh logic, so you don't have to worry about staleness.
You can then call methods like get_types, get_sources, get_countries, etc. to reason about the data.
from verifyvat_sdk import DomainGraph
graph = DomainGraph(client=client)
# Get all supported ID types (with optional filters)
types = graph.get_types(
# All filters are optional and can be combined as needed
country=("BE", "DE", "GB"), # only ID types from these countries
region=("EU", "EMEA"), # only ID types from these regions
validation=("registry", "syntactic"), # only ID types with these validation modes
coverage="full", # only ID types with full coverage
source="eu-vies", # only ID types backed by this source
)
for t in types:
print(f"{t['id']} ({t['name']}): {t['validation']} validation, {len(t['sources'])} sources")
# Get all integrated data sources (with optional filters)
sources = graph.get_sources(
id=("be-bce", "eu-vies", "no-brreg"), # only these specific sources
country=("BE", "NL", "NO"), # only sources covering these countries
region="EU", # only sources from this region
id_type=("be_vat", "nl_vat", "no_vat"), # only sources that back these ID types
coverage=("full", "partial"), # only sources with this coverage level
active=True, # only integrations that are currently active on the API
)
for s in sources:
print(
f"{s['id']} ({s['name']}): covers {', '.join(s['jurisdictions'])} with {len(s['types'])} ID types"
)
# Get all countries for which the API has coverage (with optional filters)
countries = graph.get_countries(
origin=("types", "sources"), # gather country data from these datasets (only "types", only "sources", or both)
region=("EU",), # only countries from this region
coverage="full", # only countries with at least one ID type with full coverage
validation="registry", # only countries with at least one ID type with registry validation
)
print(f"Currently supports {len(countries)} countries: {', '.join(countries)}")
# Get a structured view per-country, combining types, sources, and coverage info (with optional filters)
views = graph.get_country_views(
country=("IT", "NO"), # only build views for these countries
region="EU", # only build views for countries in this region
coverage="partial", # only consider types with at least partial coverage
validation=("registry", "syntactic"), # only consider types with these validation modes
shape="dict", # return a dict keyed by country code, or a list of views (default)
)
for country in views:
view = views[country]
print(
f"{view['country']}: {len(view['types'])} types, {len(view['sources'])} sources, {len(view['full'])} with full coverage"
)
# Get coverage level for an ID type in a specific country (with optional filters)
coverage = graph.get_coverage(
country="IT", # country code to check coverage for (required)
type="it_vat", # ID type code to check coverage for (required)
region=("EU", "EMEA"), # optional region constraint
validation="registry", # optional validation mode constraint
)
match coverage:
case "none":
print("No coverage: cannot verify this ID type in this country")
case "partial":
print(
"Partial coverage: can verify some IDs of this type in this country, but not all"
)
case "full":
print("Full coverage: can verify all IDs of this type in this country")
Offline usage and deployment snapshot
The DomainGraph is built to be used online with the API, but you can also export its data and use it offline if needed. A typical use case for this is to "bake" a snapshot of the domain graph into a deployment, and refresh it periodically (e.g. daily or weekly) via a background job.
from verifyvat_sdk import DomainGraph
online_graph = DomainGraph(client=client)
# Extract the raw data object that DomainGraph is built on
snapshot = online_graph.data()
# Save this snapshot to a file, a database, or where makes sense for you:
# save_snapshot(snapshot)
# Later, you can load this snapshot and create a DomainGraph instance from it.
# This instance will never access the network, but will still give you access to all the helper methods like get_types, get_sources, etc.
offline_graph = DomainGraph.from_data(snapshot)
For more details on how and when to use this features, see Domain graph.
Entities: selecting the right name/address/identifier
When verification returns an entity, you usually need to display only one name, one address, or one identifier, but the entity contains many, possibly across time.
This is where the SDK's entity pickers come in: they help you select the best entry based on a set of opinionated rules, without having to write custom logic for every edge case.
from verifyvat_sdk import Entity
if not verification.entity:
raise RuntimeError("No entity data returned")
entity = Entity(verification.entity)
name = entity.pick_best_name()
address = entity.pick_best_address()
print(name["value"] if name else None, address["value"] if address else None)
The selection DSL
All pickers use the same underlying DSL to express rules, which is based on five main concepts:
filter: which records are eligibleprefer: what should be prioritisedavoid: what should be penalisedrank: tie-breaking rules (especially time-based)strategy: escape hatch for custom rules that can't be expressed with the above
This is consistent across:
- names (
get_names,find_name,pick_best_name,pick_best_names) - addresses (
get_addresses,find_address,pick_best_address,pick_best_addresses) - identifiers (
get_identifiers,find_identifier,pick_best_identifier,pick_best_identifiers) - registrations (
get_registrations,find_registration,pick_best_registration,pick_best_registrations) - status records (
get_status_records,find_status_record,pick_best_status_record,pick_best_status_records) - timeline events (
get_timeline_events,find_timeline_event,pick_best_timeline_event,pick_best_timeline_events)
get_xs and find_x methods support filter only, and return all matching records (the former returns a list, the latter a single record or None).
pick_best_xs and pick_best_x methods support all five concepts and return the best matching records (the former returns a list, the latter a single record or None).
Example 1: pick a display name for a UI card
from verifyvat_sdk import Entity
entity = Entity(verification.entity)
display_name = entity.pick_best_name(
prefer={
# Prefer names with an English language tag
"language": "en",
# Prefer legal names, then short names, then trading names
"kind": ("legal", "short", "trading"),
},
avoid={
# Avoid names that are empty or very short
"value": lambda name: (not name) or (len(name) < 2),
},
rank={
# As a tiebreaker, prefer the most recent name that is still valid today (i.e. not expired)
"until": ("null", "latest"),
},
)
Example 2: pick the address, but avoid mailing ones
from verifyvat_sdk import Entity
entity = Entity(verification.entity)
addr = entity.pick_best_address(
avoid={"kind": "mailing"},
)
Example 3: pick the best identifier for a VAT scheme
from verifyvat_sdk import Entity
entity = Entity(verification.entity)
id_ = entity.pick_best_identifier(
prefer={
# We look for ID types with VAT/GST/TIN hints in their code
"id": lambda v: bool(v) and bool(re.search(r"_(vat|gst|tin)$", v, re.IGNORECASE)),
},
rank={
# We rank higher identifiers linked to registrations that are currently valid, or if not possible, that expired last
"registrations": {
"until": ("null", "latest"),
},
},
)
Time travel: "what was true on a date?"
Many entity records have validity windows (since / until). The SDK exposes a derived timeframe field, and allows you to filter and rank based on time in a intuitive way.
Pick a name valid on a specific day
This snippet will consider only names that were valid on May 1st, 2021, and among those will pick the best one based on the other rules (not shown here for simplicity).
name_on_date = entity.pick_best_name(
filter={
"on": "2021-05-01",
}
)
Pick the address valid during a period
This snippet will consider only addresses that were valid at some point during 2021, and among those will pick the best one based on the other rules (not shown here for simplicity).
addr_in_period = entity.pick_best_address(
filter={
"between": ("2021-01-01", "2021-12-31"),
# or alternatively:
"on_or_after": "2021-01-01",
"on_or_before": "2021-12-31",
}
)
Change the date comparison logic
By using the pickers' time-related advanced options, you can:
- change the date comparison granularity (day, month, year)
- match against a specific timezone
- specify custom date input formats
This snippet will consider only names that were valid in May 2021 in the Europe/London timezone, and among those will prefer legal names over others.
name_on_date_in_timezone = entity.pick_best_name(
filter={
"on": "05/01/2021",
},
prefer={
"kind": "legal",
},
time_zone="Europe/London",
date_granularity="month",
date_input_format="MM/DD/YYYY",
)
Date comparison utilities
If you need to build a custom time-based rule that can't be easily expressed with the pickers' options, you can use the SDK's public time utilities to compare validity windows against specific dates or periods.
In particular, you can use:
parse_calendar_literalandto_calendar_labelto parse and format dates in the SDK's calendar label format (e.g. "2021-05" for May 2021)compare_labelsandcompare_datesas standard comparators that understand the SDK's calendar label semantics (e.g. "2021-05" is before "2021-06", and "2021" is before "2021-05")is_on_date,is_before_date,is_on_or_before_date,is_after_date,is_on_or_after_dateto check if a validity window is active on, before, or after a specific dateis_within_rangeandis_between_rangeto check if a validity window overlaps with a specific period
If you want to know more about how date are handled by our API, see Data normalisation.
Error handling
Most of the time you only need two strategies:
Strategy A:
throw and catch VerifyVatError at the edges of your system (e.g. API route handlers, background job processors, etc.) to map to your error handling and retry logic.
from verifyvat_sdk import VerifyVatError, verify_id
try:
v = verify_id(client, id="09446231", country="GB")
except VerifyVatError as err:
# map to your API error shape
raise
Strategy B: suppress errors into the response object when you want to treat them as data (e.g. in bulk operations), and check for them in-band when processing results.
from verifyvat_sdk import verify_many_ids
results = verify_many_ids(
client,
items=inputs,
suppress_errors=True,
)
# results include in-band failures; your loop never crashes
To learn more about it, see Error handling and Bulk verification.
License
MIT License
Copyright (c) 2025 - 2026 RS1 Project
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file verifyvat_sdk-0.1.0.tar.gz.
File metadata
- Download URL: verifyvat_sdk-0.1.0.tar.gz
- Upload date:
- Size: 67.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1619ff5b2b3f4e7f692e42d98550dad499bea6564721de9385396508e85ffb4
|
|
| MD5 |
4b4a8691c7dd571793979ca187950d99
|
|
| BLAKE2b-256 |
accd0e240a36e8dd320e1bf58b636f33cee56cea05c14e77f4f6f9dcaea69abd
|
File details
Details for the file verifyvat_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: verifyvat_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 92.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c0c1bf1fa552296fba8acd493c82534e23be3c3550b8d8f4faec30cf834836f
|
|
| MD5 |
8c5c9b3fabdcc5bf8e33fa37e5353edc
|
|
| BLAKE2b-256 |
1ad42b66cf596b690ff0ab7d4dc1634c28fb80346c0f27308eed12e50d3b171e
|