How to Validate Spanish NIF and NIE Numbers in Node.js
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 Node.js, including the control letter checksum, with a single API call.
In this guide
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.
| Type | Format | Length | Example |
|---|---|---|---|
| DNI | 8 digits + 1 letter | 9 characters | 12345678Z |
| NIE | X/Y/Z + 7 digits + 1 letter | 9 characters | X1234567L |
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:
And the NIE X1234567L:
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.
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.
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. Node.js code example
Using the IsValid SDK or the native fetch API.
import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // DNI (Spanish citizen) const dni = await iv.es.nif('12345678Z'); console.log(dni.valid); // true console.log(dni.type); // 'DNI' console.log(dni.number); // '12345678' console.log(dni.letter); // 'Z' // NIE (Foreign resident) const nie = await iv.es.nif('X1234567L'); console.log(nie.valid); // true console.log(nie.type); // 'NIE' // Invalid control letter const bad = await iv.es.nif('12345678A'); console.log(bad.valid); // false
In an Express.js onboarding handler, you might use it like this:
// app.js (Express) import express from 'express'; import { createClient } from '@isvalid-dev/sdk'; const app = express(); app.use(express.json()); const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); app.post('/onboard', async (req, res) => { const { nif } = req.body; try { const result = await iv.es.nif(nif); if (!result.valid) { return res.status(400).json({ error: 'Invalid NIF/NIE number' }); } // Optionally check the type matches your expectations if (result.type === 'NIE') { // Foreign resident — may need additional documentation console.log('NIE detected — foreign resident onboarding'); } // Store the parsed components const user = await createUser({ nifType: result.type, nifNumber: result.number, nifLetter: result.letter, rawNif: nif, }); return res.json({ success: true, userId: user.id }); } catch (err) { return res.status(502).json({ error: 'NIF validation service unavailable' }); } });
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 }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the NIF/NIE passes format and checksum validation |
| type | string | "DNI" for Spanish citizens or "NIE" for foreign residents |
| number | string | The numeric portion of the identifier (8 digits for DNI, prefix + 7 digits for NIE) |
| letter | string | The control letter computed via the mod-23 algorithm |
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 const a = await iv.es.nif('00123456Y'); const b = await 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
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.