DEV Community

Antonio Garrido
Antonio Garrido

Posted on

How to validate Spanish NIF, NIE, CIF and IBAN in Python

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
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

Output:

12345678Z → NIF ✓ valid
X1234567L → NIE ✓ valid
A12345674 → CIF ✓ valid
ES9121000418450200051332 → IBAN ✓ valid
Enter fullscreen mode Exit fullscreen mode

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'])}")
Enter fullscreen mode Exit fullscreen mode

Output:

Total: 5
Valid: 4
Invalid: 1

✓ 12345678Z → NIF
✓ X1234567L → NIE
✓ A12345674 → CIF
✓ ES9121000418450200051332 → IBAN
✗ 99999999R → Formato inválido
Enter fullscreen mode Exit fullscreen mode

Understanding the Response

Each result includes:

{
  "index": 0,
  "value": "12345678Z",
  "requested_type": "AUTO",
  "detected_type": "NIF",
  "valid": true,
  "formatted": "12345678Z",
  "errors": []
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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"}
    ]
}
Enter fullscreen mode Exit fullscreen mode

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)