🐍 Python🇪🇸 SpainTax Compliance

Spanish Business Compliance — NIF, NIE & VAT Validation

Validate Spanish tax identifiers — NIF (residents), NIE (foreign individuals), and EU VAT numbers — in Python using asyncio.gather for concurrent checks and cross-validation.

Also available in Node.js

1. Spanish tax identifiers

IdentifierEndpointWho uses it
NIF/v0/nifSpanish citizens and businesses (DNI or CIF)
NIE/v0/nieForeign individuals resident in Spain
VAT (Spanish)/v0/vatEU VAT = "ES" prefix + NIF/CIF number

2. Validate NIF

Use await iv.nif(value) (SDK) or GET /v0/nif?value=….

{
  "valid": true,
  "type": "DNI",
  "number": "12345678",
  "letter": "Z"
}
  1. Check valid — mod-23 check letter
  2. Check typeDNI (individual) or CIF (company)

3. Validate NIE

Use await iv.nie(value) (SDK) or GET /v0/nie?value=…. Format: X/Y/Z + 7 digits + check letter.

{
  "valid": true,
  "type": "NIE",
  "number": "X1234567",
  "letter": "L"
}
💡Route addresses starting with X, Y, or Z to the NIE endpoint; all others to NIF.

4. Validate Spanish VAT

Use await iv.vat(value) (SDK) or GET /v0/vat?value=….

{
  "valid": true,
  "countryCode": "ES",
  "identifier": "B12345674",
  "formatted": "ESB12345674"
}
  1. Check valid and country_code == "ES"
  2. Use formatted for invoices and VIES

5. Parallel validation with asyncio.gather

import asyncio
from isvalid_sdk import IsValidConfig, create_client

config = IsValidConfig(api_key="YOUR_API_KEY")
iv = create_client(config)

async def validate_spanish_entity(entity_type: str, data: dict):
    tasks = {}
    if data.get("nif"): tasks["nif"] = iv.nif(data["nif"])
    if data.get("nie"): tasks["nie"] = iv.nie(data["nie"])
    if data.get("vat"): tasks["vat"] = iv.vat(data["vat"])

    results = dict(zip(tasks.keys(), await asyncio.gather(*tasks.values())))

    if "nif" in results and "vat" in results:
        nif_r = results["nif"]
        vat_r = results["vat"]
        if nif_r.valid and vat_r.valid:
            nif_number = f"{nif_r.number}{nif_r.letter or ''}"
            if vat_r.identifier != nif_number:
                raise ValueError(
                    f"VAT identifier ({vat_r.identifier}) does not match NIF ({nif_number})"
                )

    return results

async def main():
    result = await validate_spanish_entity("business", {
        "nif": "B12345674",
        "vat": "ESB12345674",
    })
    for k, v in result.items():
        print(f"{k}: valid={v.valid}")

asyncio.run(main())

6. Edge cases

Auto-routing NIF vs NIE

async def validate_any_spanish_personal_id(value: str):
    upper = value.upper()
    if upper[0] in ("X", "Y", "Z"):
        return {"type": "NIE", "result": await iv.nie(upper)}
    return {"type": "NIF", "result": await iv.nif(upper)}

NIF/VAT cross-check

⚠️Spanish VAT = "ES" + NIF. Cross-check both when a business provides both identifiers.
nif_r, vat_r = await asyncio.gather(iv.nif(nif), iv.vat(vat))
nif_number = f"{nif_r.number}{nif_r.letter or ''}"
if nif_r.valid and vat_r.valid and vat_r.identifier != nif_number:
    raise ValueError("NIF and VAT do not match")

7. Summary checklist

Route X/Y/Z-prefixed IDs to NIE endpoint
Validate NIF format and mod-23 check letter
Validate full ESxxxx VAT for B2B invoicing
Cross-check VAT identifier against NIF number
Run all calls with asyncio.gather
Distinguish DNI vs CIF via type field
Use formatted VAT for VIES submissions
Return 422 with field-level error messages

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →