How to Validate Spanish NIF, NIE, CIF and IBAN in Python
If you've ever built a form or backend system for the Spanish market, you've likely had to validate fiscal identifiers like NIF, NIE, CIF or IBAN. Each one has its own algorithm, edge cases, and validation rules — and implementing them correctly from scratch is surprisingly tricky.
In this article, I'll show you how to validate all four identifier types with a single API call using Python, without having to implement or maintain the validation algorithms yourself.
The Problem with DIY Validation
Spanish fiscal identifiers are more complex than they look:
- NIF: 8 digits + 1 letter, validated with a modulo-23 algorithm
- NIE: Starts with X, Y or Z, then follows the NIF algorithm with substitution
- CIF: Letter + 7 digits + control character (can be a letter or digit depending on the entity type)
- IBAN: ES + 2 check digits + 20-digit BBAN, validated with MOD-97
Most implementations online have subtle bugs — incorrect CIF letter validation, missing NIE edge cases, or IBAN check digit errors. Getting it wrong means accepting invalid identifiers or rejecting valid ones.
A Better Approach: Use an API
Valix is a REST API that validates Spanish fiscal identifiers using the official algorithms. It handles NIF, NIE, CIF and IBAN, detects the type automatically, and returns a structured JSON response.
You can try it for free — no registration, no API key required for the trial endpoint.
Getting Started
No libraries to install beyond requests:
pip install requests
Trial: Validate Without Registration
The trial endpoint allows up to 5 identifiers per call and 50 validations per day per IP — no API key needed.
import requests
def validate_identifiers_trial(identifiers: list[str]) -> dict:
"""
Validate Spanish fiscal identifiers using the Valix trial endpoint.
No API key required. Max 5 identifiers per call, 50/day per IP.
"""
url = "https://api.getvalix.io/v1/validate/trial"
payload = {
"items": [
{"value": identifier, "type": "AUTO"}
for identifier in identifiers
]
}
response = requests.post(url, json=payload)
response.raise_for_status()
return response.json()
# Example usage
identifiers = [
"12345678Z", # NIF
"X1234567L", # NIE
"A12345674", # CIF
"ES9121000418450200051332" # IBAN
]
result = validate_identifiers_trial(identifiers[:5]) # Max 5 for trial
for item in result["results"]:
status = "✓ valid" if item["valid"] else "✗ invalid"
print(f"{item['value']} → {item['detected_type']} {status}")
Output:
12345678Z → NIF ✓ valid
X1234567L → NIE ✓ valid
A12345674 → CIF ✓ valid
ES9121000418450200051332 → IBAN ✓ valid
Production: Batch Validation with API Key
For production use, the batch endpoint supports up to 100 identifiers per call. Get your API key at getvalix.io.
import requests
import os
def validate_batch(identifiers: list[str], api_key: str) -> dict:
"""
Validate up to 100 Spanish fiscal identifiers in a single API call.
Requires a valid API key from getvalix.io.
"""
url = "https://api.getvalix.io/v1/validate/batch"
headers = {
"x-api-key": api_key,
"Content-Type": "application/json"
}
payload = {
"items": [
{"value": identifier, "type": "AUTO"}
for identifier in identifiers
]
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
# Load API key from environment variable (never hardcode it)
api_key = os.environ.get("VALIX_API_KEY")
identifiers = [
"12345678Z",
"X1234567L",
"A12345674",
"ES9121000418450200051332",
"99999999R", # Invalid NIF
]
result = validate_batch(identifiers, api_key)
print(f"Total: {result['total']}")
print(f"Valid: {result['valid_count']}")
print(f"Invalid: {result['invalid_count']}")
print()
for item in result["results"]:
if item["valid"]:
print(f"✓ {item['value']} → {item['detected_type']}")
else:
print(f"✗ {item['value']} → {', '.join(item['errors'])}")
Output:
Total: 5
Valid: 4
Invalid: 1
✓ 12345678Z → NIF
✓ X1234567L → NIE
✓ A12345674 → CIF
✓ ES9121000418450200051332 → IBAN
✗ 99999999R → Formato inválido
Understanding the Response
Each result includes:
{
"index": 0,
"value": "12345678Z",
"requested_type": "AUTO",
"detected_type": "NIF",
"valid": true,
"formatted": "12345678Z",
"errors": []
}
-
detected_type: the actual identifier type (NIF, NIE, CIF, IBAN) -
valid: boolean result -
formatted: normalized version of the identifier -
errors: list of validation error messages if invalid
For CIF identifiers, the response also includes entity_type — the type of Spanish business entity (e.g. "Sociedad Anónima").
Handling Errors Gracefully
import requests
from requests.exceptions import HTTPError
def validate_with_error_handling(identifiers: list[str], api_key: str) -> dict | None:
try:
result = validate_batch(identifiers, api_key)
return result
except HTTPError as e:
if e.response.status_code == 401:
print("Invalid or missing API key")
elif e.response.status_code == 429:
print("Monthly validation limit exceeded")
elif e.response.status_code == 400:
error_data = e.response.json()
print(f"Bad request: {error_data.get('error')}")
else:
print(f"API error: {e}")
return None
Type-Specific Validation
If you already know the type of identifier you're validating, you can pass it explicitly instead of using AUTO:
payload = {
"items": [
{"value": "12345678Z", "type": "NIF"},
{"value": "X1234567L", "type": "NIE"},
{"value": "A12345674", "type": "CIF"},
{"value": "ES9121000418450200051332", "type": "IBAN"}
]
}
This is useful when your data source already provides the identifier type, or when you want to enforce a specific type validation.
Pricing
- Trial: Free, 50 validations/day, no registration
- Starter: €19/month — 10,000 validations/month
- Growth: €49/month — 100,000 validations/month
- Pro: €149/month — 1,000,000 validations/month
Summary
Validating Spanish fiscal identifiers correctly requires implementing multiple complex algorithms with edge cases that are easy to get wrong. Using Valix gives you:
- Accurate validation based on official algorithms
- Automatic type detection
- Batch processing up to 100 identifiers per call
- A free trial to test before committing
Try it at getvalix.io — the trial endpoint requires no registration.
Top comments (0)