Validate VAT Numbers from a CSV with Python

Read VAT numbers from a CSV, validate them with the Avatcado batch API, and write the results to a new file. The script below keeps each input row and records individual lookup errors.

Prepare the input file

You need Python 3.9 or later, SDK 0.6.0, and a Pro or Business account. Batch validation requires a paid plan, including when you use a test key. These examples were checked with Python 3.14.3. Create an account to get your API key and choose a batch-enabled plan.

Save the following as input.csv in UTF-8. The header must contain vat_number. Additional columns are ignored. Quoted CSV values and a UTF-8 byte-order mark are supported.

vat_number
DE111111111
DE000000000
DE123
DE111111111
python -m pip install avatcado==0.6.0
export AVATCADO_API_KEY="avat_test_replace_with_your_key"

Use a test key from your Pro or Business account. On PowerShell, set the key with $env:AVATCADO_API_KEY="avat_test_replace_with_your_key".

Run the CSV validation script

Save this as validate_csv.py. Keep the API key in your environment.

import argparse
import csv
from itertools import islice
import os
from pathlib import Path
import sys
from avatcado import Avatcado, AvatcadoError, is_batch_success

FIELDS = ["input_number", "normalized_number", "status",
          "company_name", "source", "error_code"]

def check_rows(client, rows):
    numbers = [(row.get("vat_number") or "").strip() for row in rows]
    nonempty = [number for number in numbers if number]
    results = iter(client.vat.validate_batch(nonempty).results) if nonempty else iter(())
    output = []
    for row, number in zip(rows, numbers):
        record = dict.fromkeys(FIELDS, "")
        record["input_number"] = row.get("vat_number") or ""
        record["status"] = "error"
        if not number:
            record["error_code"] = "missing_vat_number"
        else:
            item = next(results)
            if is_batch_success(item):
                record["normalized_number"] = item.data.vat_number
                record["status"] = "valid" if item.data.valid else "invalid"
                record["company_name"] = (item.data.company.name or "") if item.data.company else ""
                record["source"] = item.meta.source or ""
            else:
                record["normalized_number"] = item.error.vat_number or ""
                record["error_code"] = item.error.code
        output.append(record)
    return output

def main():
    parser = argparse.ArgumentParser(description="Validate a CSV of VAT numbers")
    parser.add_argument("input", type=Path)
    parser.add_argument("output", type=Path)
    args = parser.parse_args()
    if args.input.resolve() == args.output.resolve():
        parser.error("Input and output must use different filenames")
    key = os.environ.get("AVATCADO_API_KEY")
    if not key:
        parser.error("Set AVATCADO_API_KEY before running the script")

    completed = 0
    try:
        with args.input.open(newline="", encoding="utf-8-sig") as source:
            reader = csv.DictReader(source)
            if not reader.fieldnames or "vat_number" not in reader.fieldnames:
                parser.error("The input CSV needs a vat_number column")
            with args.output.open("x", newline="", encoding="utf-8") as target:
                writer = csv.DictWriter(target, fieldnames=FIELDS)
                writer.writeheader()
                target.flush()
                with Avatcado(key, timeout=60.0) as client:
                    while rows := list(islice(reader, 50)):
                        output = check_rows(client, rows)
                        writer.writerows(output)
                        target.flush()
                        completed += len(output)
    except AvatcadoError as error:
        print(f"Stopped after {completed} input rows: {error.code}: {error.message}. "
              "Completed rows remain in the output file.", file=sys.stderr)
        return 1
    except (OSError, csv.Error) as error:
        print(f"File error: {error}", file=sys.stderr)
        return 1
    print(f"Wrote {completed} rows to {args.output}")
    return 0

if __name__ == "__main__":
    sys.exit(main())
python validate_csv.py input.csv results.csv

Choose a new output filename. The script refuses to overwrite an existing file. It sends at most 50 numbers in each request and writes completed groups to disk. Empty values receive missing_vat_numberwithout an API request. Empty physical lines are skipped by the CSV reader; use a quoted empty value ("") for a row that contains an empty VAT field.

Read the output

The test input above produces these columns and results:

input_number,normalized_number,status,company_name,source,error_code
DE111111111,DE111111111,valid,"Test GmbH",test,
DE000000000,DE000000000,invalid,,test,
DE123,DE123,error,,,invalid_vat_format
DE111111111,DE111111111,valid,"Test GmbH",test,

CSV writers may omit quotes around values that contain no commas or quotes. The column values remain the same. Company names can be empty. A successful lookup with valid: false produces an invalid row. Malformed input and failed lookups produce an error row.

The output preserves duplicates and input order. The API deduplicates normalized numbers within a batch for quota counting, then returns a result for every submitted item. Repeated numbers in separate batches can consume quota again. The source column identifies the source of successful results.

Handle a failed batch request

An authentication error, insufficient plan, quota limit, or request-level failure stops the script. Its message reports the number of input rows already written. Those rows remain in the output file. The current group and remaining input rows have no output yet.

Resolve the reported error, then copy the unprocessed input rows into a new CSV with the same header. Use the completed row count to locate the next input record. Run the script again with a new output filename. The sample does not retry requests automatically.

Rerun individual errors

Filter completed results to status=error. Correct missing or malformed numbers, then copy the input_number values into a new file with a vat_number header. Retry unavailable registries later. Keep the original result file for reference.

Validate live records

Replace the test key with your avat_live_ key and use your customer or supplier VAT numbers. Live checks use your plan quota. Treat imported CSV content as data when opening the output in a spreadsheet; disable formula evaluation for untrusted values.

See the Python single-number guide for SDK setup and exceptions. For background jobs, use async batch validation. The CRM guide covers updating customer records after validation.

Sources

Related guides