verifyvat-sdk 0.1.0
Official Python SDK for VerifyVAT
Requires Python: >=3.13

# 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:

1. **Safe API calls**:  
   Authentication, retries with backoff, envelope validation, and a single error type with useful context for debugging.
2. **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.
3. **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](https://verifyvat.com/docs) to understand the API's concepts and features, then come back here for SDK-specific usage and examples.

---

## Installation

```bash
pip install verifyvat-sdk
```

Python requirement: **≥ 3.13**.

---

## Quickstart

```py
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:

```properties
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.

```py
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.

```py
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.

```py
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:

```py
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](https://verifyvat.com/docs/response-envelope).

---

## Verify an ID

Verification answers: **"what did sources return, and how complete/reliable was it?"**

```py
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.

```py
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.

```py
v = verifier.verify_id(id="09446231", country="GB", suppress_errors=True)
```

For more on error handling strategies and request failures, see [Error handling](https://verifyvat.com/docs/error-handling).

---

## Bulk verification (concurrency + per-item outputs)

Use `verify_many_ids` when you have a small batch of inputs to verify at once.

```py
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_errors` is 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.

```py
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](https://verifyvat.com/docs/sdk/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.

```py
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.

```py
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.

```py
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](https://verifyvat.com/docs/sdk/verify-and-decide).  
If you want to understand what stale and fresh data means and how to reason about it, see [Caching and stale data](https://verifyvat.com/docs/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](https://verifyvat.com/docs/infer-id-type).

```py
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:

```py
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](https://verifyvat.com/docs/list-supported-ids) and [List data sources](https://verifyvat.com/docs/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.

```py
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.

```py
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](https://verifyvat.com/docs/sdk/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.

```py
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:

1. `filter`: which records are eligible
2. `prefer`: what should be prioritised
3. `avoid`: what should be penalised
4. `rank`: tie-breaking rules (especially time-based)
5. `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

```py
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

```py
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

```py
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).

```py
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).

```py
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.

```py
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_literal` and `to_calendar_label` to parse and format dates in the SDK's calendar label format (e.g. "2021-05" for May 2021)
- `compare_labels` and `compare_dates` as 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_date` to check if a validity window is active on, before, or after a specific date
- `is_within_range` and `is_between_range` to 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](https://verifyvat.com/docs/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.

```py
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.

```py
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](https://verifyvat.com/docs/error-handling) and [Bulk verification](https://verifyvat.com/docs/sdk/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.