Guide · Python · SDK · REST API

Spanish NIF and NIE Validation in Python

NIF and NIE are the tax identification numbers used in Spain — one for citizens, the other for foreign residents. Here's how to validate both formats properly in Python, including the control letter checksum, with a single API call.

1. What are NIF and NIE numbers?

Spain uses the NIF (Número de Identificación Fiscal) as the umbrella term for tax identification numbers. In practice, there are two main types for individuals:

DNI — Documento Nacional de Identidad

Issued to Spanish citizens. The DNI number is 8 digits followed by a control letter (e.g. 12345678Z). It serves as both the national ID and the tax number. Every Spanish citizen over 14 is required to have one.

NIE — Número de Identidad de Extranjero

Issued to foreign residents and non-residents who have economic, professional, or social dealings in Spain. It starts with a letter (X, Y, or Z) followed by 7 digits and a control letter (e.g. X1234567L).

Both formats share the same control letter algorithm and are validated against the same lookup table. Any system that deals with Spanish customers, employees, or tax filings needs to handle both.


2. The structure of NIF/NIE

The two formats are closely related. Both result in a number between 0 and 99,999,999 that is divided by 23 to determine the control letter.

DNI format12345678Z
12345678 = 8 digitsZ = control letter
NIE formatX1234567L
X/Y/Z = prefix letter1234567 = 7 digitsL = control letter
TypeFormatLengthExample
DNI8 digits + 1 letter9 characters12345678Z
NIEX/Y/Z + 7 digits + 1 letter9 charactersX1234567L
ℹ️For NIE numbers, the prefix letter maps to a numeric value: X = 0, Y = 1, Z = 2. This value is prepended to the 7 digits before computing the control letter, producing an effective 8-digit number — the same as a DNI.

3. The control letter algorithm

The control letter is computed by dividing the numeric portion by 23 and looking up the remainder in a fixed table. For a DNI, the numeric portion is the 8-digit number itself. For a NIE, replace the prefix letter (X→0, Y→1, Z→2) and concatenate with the 7 digits.

Lookup table (remainder → letter)

0=T  1=R  2=W  3=A  4=G  5=M  6=Y  7=F  8=P  9=D  10=X  11=B

12=N  13=J  14=Z  15=S  16=Q  17=V  18=H  19=L  20=C  21=K  22=E

For example, the DNI 12345678Z:

12345678 mod 23 = 14 → letter "Z" ✓

And the NIE X1234567L:

X → 0, so the number becomes 01234567
1234567 mod 23 = 19 → letter "L" ✓

4. Why you need both format and checksum validation

Format alone is not enough

A regex like /^[0-9]{8}[A-Z]$/ will accept 12345678A, but the correct letter for that number is Z, not A. Without verifying the checksum, you accept typos and fabricated numbers.

Checksum alone is not enough

The mod-23 check catches single-character errors but does not verify that the number was actually issued. It also does not distinguish between DNI and NIE or catch structural issues like leading zeros in legacy formats.

Tax compliance and KYC

Spanish tax filings (modelo 303, 347, etc.) require valid NIF numbers. Submitting incorrect identifiers to the Agencia Tributaria results in rejected declarations and potential penalties. For KYC onboarding, you need to confirm the NIF type matches the expected residency status.

⚠️A simple regex gives a false sense of security. Always verify the control letter checksum and ideally distinguish between DNI and NIE types to catch mismatched residency claims.

5. The right solution

The IsValid NIF API handles format validation, control letter verification, and type detection (DNI vs NIE) in a single GET request. Pass the NIF/NIE value and get back a structured response with the validation result, document type, parsed number, and control letter.

DNI + NIE
Formats
citizens and foreign residents
mod-23
Checksum
control letter verified
100/day
Free tier
no credit card

Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.

Full parameter reference and response schema: NIF Validation API docs →


6. Python code example

Using the isvalid-sdk Python SDK or the requests library. Install with pip install isvalid-sdk or pip install requests.

# nif_validator.py
import os
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))

# DNI (Spanish citizen)
dni = iv.es.nif("12345678Z")
print(dni["valid"])    # True
print(dni["type"])     # 'DNI'
print(dni["number"])   # '12345678'
print(dni["letter"])   # 'Z'

# NIE (Foreign resident)
nie = iv.es.nif("X1234567L")
print(nie["valid"])    # True
print(nie["type"])     # 'NIE'

# Invalid control letter
bad = iv.es.nif("12345678A")
print(bad["valid"])    # False

In a Flask onboarding handler, you might use it like this:

# app.py (Flask)
import os
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

API_KEY = os.environ["ISVALID_API_KEY"]
BASE_URL = "https://api.isvalid.dev"


def validate_nif(nif: str) -> dict:
    resp = requests.get(
        f"{BASE_URL}/v0/es/nif",
        params={"value": nif},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    resp.raise_for_status()
    return resp.json()


@app.post("/onboard")
def onboard():
    data = request.get_json()

    try:
        result = validate_nif(data["nif"])
    except requests.RequestException:
        return jsonify(error="NIF validation service unavailable"), 502

    if not result["valid"]:
        return jsonify(error="Invalid NIF/NIE number"), 400

    # Optionally check the type matches your expectations
    if result["type"] == "NIE":
        # Foreign resident — may need additional documentation
        print("NIE detected — foreign resident onboarding")

    # Store the parsed components
    user = create_user(
        nif_type=result["type"],
        nif_number=result["number"],
        nif_letter=result["letter"],
        raw_nif=data["nif"],
    )

    return jsonify(success=True, user_id=user["id"])
The API normalises input automatically — it strips whitespace, removes dashes and dots, and uppercases the value. Pass the raw user input directly without pre-processing.

7. cURL example

Validate a Spanish DNI:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/es/nif?value=12345678Z"

Validate a NIE:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/es/nif?value=X1234567L"

8. Understanding the response

Valid DNI response:

{
  "valid": true,
  "type": "DNI",
  "number": "12345678",
  "letter": "Z"
}

Valid NIE response:

{
  "valid": true,
  "type": "NIE",
  "number": "X1234567",
  "letter": "L"
}

Invalid NIF:

{
  "valid": false
}
FieldTypeDescription
validbooleanWhether the NIF/NIE passes format and checksum validation
typestring"DNI" for Spanish citizens or "NIE" for foreign residents
numberstringThe numeric portion of the identifier (8 digits for DNI, prefix + 7 digits for NIE)
letterstringThe control letter computed via the mod-23 algorithm
ℹ️The type, number, and letter fields are only present when valid is true. When the input is invalid, only valid: false is returned.

9. Edge cases

Old-format NIEs

NIE numbers issued before 2008 used only the X prefix. Later, Spain introduced Y and Z prefixes as the X-series was exhausted. Your system should accept all three prefix letters. The API handles this automatically.

Corporate CIF numbers

Companies in Spain use a CIF (Certificado de Identificación Fiscal), which has a different format: a letter indicating the entity type, followed by 7 digits and a check character (digit or letter). CIF numbers are not validated by the NIF endpoint — they are a separate identifier type. If you need to handle both personal and corporate identifiers, validate them through different paths.

Leading zeros in DNI numbers

DNI numbers below 10,000,000 have leading zeros (e.g. 00123456Y). Some users omit these zeros when entering their number. The API normalises short inputs by padding to 8 digits, but it is good practice to store the full 9-character form (8 digits + letter).

# Both inputs produce the same valid result
a = iv.es.nif("00123456Y")
b = iv.es.nif("123456Y")
# a["valid"] == True, b["valid"] == True
# a["number"] == "00123456", b["number"] == "00123456"

Input with separators

Users sometimes enter NIF/NIE with dashes, dots, or spaces (e.g. 12.345.678-Z). The API strips these automatically. Pass the raw user input without pre-processing and use the parsed response fields for storage.


10. Summary

Do not rely on regex alone — it cannot verify the control letter
Do not ignore the DNI vs NIE distinction — they indicate residency status
Do not confuse NIF/NIE with corporate CIF numbers — they use different validation rules
Validate both format and mod-23 checksum in one API call
Use the type field to distinguish DNI from NIE in your workflow
Store the parsed number and letter separately for clean data

See also

Validate Spanish NIF/NIE numbers instantly

Free tier includes 100 API calls per day. No credit card required. Format validation plus control letter checksum verification for both DNI and NIE.