Official sources
This page is SPEC.md, the specification of what the library accepts and rejects, rendered from the file each time the site is built. Every error that validate() reports names one of the rule IDs below (DNI-2, CIF-3…), and every rule has an anchor, such as #cif-3, that you can link to.
Not every rule you find online has an official source, so each rule has a tier:
- Law (T1): a legal text in force, published in the BOE (Boletín Oficial del Estado), such as the formats of the DNI, NIE, K/L/M NIF and CIF, and the CIF organisation keys.
- Official guidance (T2): a page of the administration responsible for the document, the Ministerio del Interior or the AEAT (the Spanish tax agency). The DNI check letter (mod 23) is published there and in the AEAT’s technical note (T3), but in no law.
- Semi-official (T3): the AEAT’s internal technical note on the NIF. It is the only source for which CIF keys take a digit and which take a letter.
- Convention (T4): industry practice with no official text, labelled as such. Conventions only affect input cleanup (lower case, separators), never which documents are valid, with one documented exception: no official text publishes the CIF control arithmetic (CIF-4).
The specification itself is written in English. To propose a change, see How to propose a change.
This document is the authoritative specification of what nif-dni-nie-cif-validation accepts and rejects, and why. It is the library’s own document, not a government one: every rule has an ID, a source tier and a citation, so any behaviour of the library can be traced back to the official text that justifies it (or be labelled as convention when no official text exists).
Last verified: 2026-09-30
How to use rule IDs
Section titled “How to use rule IDs”- Each rule below has an ID made of a family and a number:
DNI-1,KLM-2,NIE-3,CIF-3,VAT-1,NORM-1. - Code comments and test names reference these IDs, for example
// CIF-3 (AEAT D.I.T. 2008). Every validation branch insrc/should cite the rule it implements, and every ID used in the code must exist in this file with the same meaning. - Each ID has an anchor, so it can be linked directly:
SPEC.md#cif-3,SPEC.md#nie-3. - IDs are stable. If a rule changes, update its text and keep the ID. If a rule is dropped, keep the ID retired in this file rather than reusing the number.
How to propose a change
Section titled “How to propose a change”- Open an issue that names the rule ID (or proposes a new one) and cites the source: the law (article), the official page, or the document. Conventions with no official text are accepted only as documented T4 rules (see the tiers below).
- Update this file in the same pull request as the code change, bump the “Last verified” date, and keep the code comments and test names in sync with the IDs.
- A change that makes the library accept fewer or more inputs by default is a breaking change (major version).
Source tiers
Section titled “Source tiers”| Tier | Meaning | Sources |
|---|---|---|
| T1: Law (BOE) | Published legal text in force | RD 1065/2007 (RGAT) arts. 19–22, 24, 25 · Orden EHA/451/2008 (as amended by Orden HAP/5/2016) · Orden INT/2058/2008 (in Orden 7/2/1997, apartado sexto.a) · RD 255/2025 (DNI; replaced RD 1553/2005 on 2025-04-02) · RD 1155/2024 art. 205 (NIE; replaced RD 557/2011 on 2025-05-20) |
| T2: Official government page | Published by the responsible administration | Ministerio del Interior, “Cálculo del dígito de control del NIF/NIE” · AEAT Sede, “Composición del NIF” (personas físicas / personas jurídicas) |
| T3: Semi-official | Internal AEAT technical document | AEAT D.I.T. note “Número de Identificación Fiscal (N.I.F.)”, updated 2008-04-15, hosted by CAIB (see Source URLs) |
| T4: Convention | Industry practice with no official text | Documented. Never on by default unless it only affects input cleanup, with one documented exception: no official text publishes the CIF control arithmetic, so the universal algorithm applies by default (CIF-4). |
Every NIF
Section titled “Every NIF”| ID | Rule | Tier | Source |
|---|---|---|---|
| NIF-1 | The first character selects exactly one format: a digit (DNI-1), K L M (KLM-1), X Y Z (NIE-1) or an organisation key (CIF-2). These sets never overlap. Any other first character (for example T, see Explicitly NOT implemented) is not a NIF |
follows from DNI-1, KLM-1, NIE-1 and CIF-2 | RD 1065/2007 arts. 19–22 |
Natural persons: DNI
Section titled “Natural persons: DNI”The mod-23 algorithm is not in any BOE text. T2 (Interior) is its most authoritative source.
Natural persons: K / L / M NIF
Section titled “Natural persons: K / L / M NIF”| ID | Rule | Tier | Source |
|---|---|---|---|
| KLM-1 | K (Spaniards under 14 living in Spain, without DNI), L (Spaniards resident abroad, without DNI), M (foreigners without NIE) + 7 characters + an alphabetic check character | T1 | RD 1065/2007 arts. 19.2 and 20.2 |
| KLM-2 | The check letter uses DNI-2 over the 7 digits only (the prefix is not converted to a digit) | T3 | AEAT D.I.T.: “…los siete dígitos siguientes a la letra inicial en el resto de los casos” |
| KLM-3 | The 7 characters are digits | T3 | The AEAT D.I.T. note says “siete dígitos numéricos”. Conflict: the BOE says “siete caracteres alfanuméricos”. We follow T3 because the check algorithm needs digits. See Known conflicts between sources. |
An M NIF can be temporary (AEAT: “válido por tres meses” while the NIE is pending). That is a registry state, not a format rule.
Natural persons: NIE
Section titled “Natural persons: NIE”Legal persons and entities: NIF (formerly CIF)
Section titled “Legal persons and entities: NIF (formerly CIF)”| ID | Rule | Tier | Source |
|---|---|---|---|
| CIF-1 | 9 characters: organisation key + 7 digits + a control character | T1 | Orden EHA/451/2008 art. 2; RD 1065/2007 art. 22.1 |
| CIF-2 | Valid keys: A B C D E F G H J N P Q R S U V W. No key has ever been removed; K, L, M, X, Y and Z are natural-person prefixes, never entities | T1 | Orden EHA/451/2008 arts. 3–5 (art. 3 as amended by Orden HAP/5/2016, in force 2016-01-16) |
| CIF-3 | Control type: digit for A B C D E F G H J U V (Spanish entities); letter for N P Q R S W | T3 | AEAT D.I.T.: “numérico (0 a 9) para ent. jurídicas nacionales. Alfabético (A…J) para entidades jurídicas extranjeras, corporaciones locales, organismos públicos y congregaciones religiosas y órganos de la Administración” |
| CIF-4 | Control arithmetic: add the digits in even positions (2, 4, 6); for the digits in odd positions (1, 3, 5, 7), double each one and add the digits of each result; control = (10 − total mod 10) mod 10; letter = JABCDEFGHI[control] |
T4 | No official text defines it (checked: Decreto 2423/1975, the Orders of 1980/85/89/98, RD 338/1990, RD 1065/2007, Orden EHA/451/2008). Universal industry consensus, checked against public-body NIFs P2807900B (Ayuntamiento de Madrid) and Q2826000H (AEAT). See Known conflicts between sources. |
| CIF-5 | The 7 digits are random. Do not validate province codes | T1 | Orden EHA/451/2008 art. 2.b “número aleatorio”; preamble: “desaparece… la información relativa a la provincia” |
VAT (intra-EU)
Section titled “VAT (intra-EU)”| ID | Rule | Tier | Source |
|---|---|---|---|
| VAT-1 | Spanish VAT number format = ES + NIF. A valid format does not mean the number is registered in VIES |
T1 | RD 1065/2007 art. 25.1 |
Input cleanup (never changes validity, only parsing)
Section titled “Input cleanup (never changes validity, only parsing)”Input contract (library behaviour, no official content)
Section titled “Input contract (library behaviour, no official content)”These rules don’t decide which documents are valid. They say how the library treats input that is not a document at all, so that every error can cite a rule.
Opt-in policies (off by default)
Section titled “Opt-in policies (off by default)”These rules are not in any official source. They never apply unless the caller asks for them, so they never change the default result, which follows only the rules above (official rules and the documented T4 conventions: input cleanup, and the CIF control arithmetic of CIF-4).
| ID | Rule | Tier | Source |
|---|---|---|---|
| POLICY-1 | Placeholder numbers 00000000T, 00000001R, 99999999R and X0000000T are valid documents by default. With rejectPlaceholders: true they are rejected (error PLACEHOLDER), in any accepted form (lower case, old NIE form X00000000T, with separators) |
T4 | ESNIC (.es registry) filters them as obviously fake; no law forbids them. See Explicitly NOT implemented |
| POLICY-2 | The caller may accept only some document types (option types). A document of another type is rejected (error UNSUPPORTED_TYPE), even when it is valid |
Library option | #56 |
Explicitly NOT implemented (no official basis)
Section titled “Explicitly NOT implemented (no official basis)”| Folklore rule | Finding | Decision |
|---|---|---|
“A CIF number starting with 00 must have a letter control” |
No official list has ever had province code 00. It traces back to an uncited Wikipedia table | Do not implement. Remove the dead code; don’t turn it on |
| “C D F G J U V accept either letter or digit” | Contradicts CIF-3 (T3) | Off by default. Opt-in cifControl: "lenient" for legacy data (decision approved 2026-09-30; breaking change in v2) |
NIE / NIF with a T prefix |
No official source | Not supported (rejected as NIF-1) |
| Province-code validation for CIFs | Repealed; random since 2008 | Not implemented |
| Rejecting 00000000T, 00000001R, 99999999R, X0000000T | No law forbids them; ESNIC (.es registry) filters them as obviously fake | Valid by default. Opt-in rejectPlaceholders: true (POLICY-1) |
Decisions
Section titled “Decisions”CIF keys C, D, F, G, J, U and V: digit-only by default from v2 (approved 2026-09-30)
Section titled “CIF keys C, D, F, G, J, U and V: digit-only by default from v2 (approved 2026-09-30)”- From v2, organisation keys C, D, F, G, J, U and V require a digit control, as CIF-3 states. This is a breaking change.
- An opt-in option
cifControl: "lenient"keeps the legacy behaviour (either a letter or a digit) for these keys, for old data. - v1.x kept accepting either a letter or a digit for C, D, F, G, J, U and V, so existing users were not broken. 2.0.0 implements the decision:
cifControldefaults to"official", and{ cifControl: "lenient" }restores the v1 behaviour (see MIGRATION.md). - The keys A, B, E and H (digit) and N, P, Q, R, S and W (letter) already follow CIF-3 in v1.
How input cleanup works (settled by the v2 API, 2026-09-30)
Section titled “How input cleanup works (settled by the v2 API, 2026-09-30)”NORM-1 to NORM-4 say what is ignored; the implementation (normalize()) settles the details:
- White space is exactly JavaScript’s
\s(spaces, tabs, line breaks, no-break spaces, the BOM…), removed anywhere, which also trims. Dots, hyphen-minus (-) and slashes are removed anywhere. Other punctuation (_,,, the en dash–) is kept, so the value stays invalid. - Case: only ASCII letters and
ñare upper-cased. Non-ASCII look-alikes thattoUpperCase()maps to ASCII letters (ı→I,ſ→S) are kept, so they never become valid. - Order: separators and case, then NIE-3 (
X0nnnnnnnL→XnnnnnnnL), then NORM-4 (1 to 7 digits and a letter are left-padded to 8 digits). normalize: falseturns off NORM-2 to NORM-4. Lower case (NORM-1) and the old NIE form (NIE-3) are still accepted, as in v1.ESis not cleanup: anESprefix makes a VAT number (VAT-1), accepted only withallowVatPrefixor byisValidSpanishVat.
Type detection is format-based (getNifType(), settled 2026-09-30)
Section titled “Type detection is format-based (getNifType(), settled 2026-09-30)”getNifType() returns the type that validate() reports: the one selected by the first character (NIF-1), once the length, the digits and the class of the control character (a letter for DNI, K/L/M and NIE; a letter or a digit for CIF) match that type’s format. It does not check the control character: 12345678A is a "DNI" and B1234567D is a "CIF", although both are invalid. So a non-null type never means “valid”.
Display format (format(), settled 2026-09-30)
Section titled “Display format (format(), settled 2026-09-30)”No official source defines a display grouping. format() splits the canonical form into the parts the rules define, joined by a separator (- by default, a space, or nothing):
| Type | Parts | Rules | Example |
|---|---|---|---|
| DNI | 8 digits · letter | DNI-1 | 12345678-Z |
| NIE | prefix · 7 digits · letter | NIE-1 | X-1234567-L |
| K/L/M NIF | prefix · 7 digits · letter | KLM-1 | K-1234567-L |
| NIF of a legal person or entity | key · 7 digits · control | CIF-1 | B-1234567-4 |
Only valid documents are formatted (default options); format() returns null otherwise and never adds an ES prefix.
Single-character substitutions (checked by the property tests)
Section titled “Single-character substitutions (checked by the property tests)”Replacing one character of a valid document (positions 2 to 9) always gives an invalid one: DNI-2 changes with every digit (10^k mod 23 is never 0), CIF-4 too (doubling a digit and adding the digits of the result is a permutation), a letter in the number breaks the format, and another control character is wrong. Replacing the first character can give another valid document, because the prefix counts for little or nothing in the control:
- K, L and M are interchangeable (KLM-2 ignores the prefix);
- organisation keys with the same control class are interchangeable (CIF-4 ignores the key);
- across types: a DNI starting with 0, 1 or 2 has the same letter as the NIE X, Y or Z (NIE-2) and, for 0, the K/L/M NIF with the same digits; other swaps can hit a control that happens to match.
With cifControl: "lenient", the digit and the letter of the same control value are both valid for C D F G J U V.
Known conflicts between sources
Section titled “Known conflicts between sources”KLM-3: “alfanuméricos” (BOE) vs “dígitos” (AEAT)
Section titled “KLM-3: “alfanuméricos” (BOE) vs “dígitos” (AEAT)”- RD 1065/2007 arts. 19.2 and 20.2 (T1) describe the K/L/M NIF as a letter, “siete caracteres alfanuméricos” and a check character.
- The AEAT D.I.T. note (T3) says “siete dígitos numéricos”.
- Decision: we follow the AEAT D.I.T. note, because the check algorithm (KLM-2) is computed over the 7 digits and is undefined for arbitrary characters. If an official source ever publishes a check algorithm for non-digit characters, KLM-3 must be revisited.
CIF-4: the control arithmetic is not published anywhere official
Section titled “CIF-4: the control arithmetic is not published anywhere official”- No official text defines the control arithmetic for legal-entity NIFs (see the list of texts checked in CIF-4). It is a T4 convention.
- It is the only T4 rule that decides validity by default: without it, the control character of a legal-entity NIF could not be checked at all.
- It is the universal industry algorithm, and it was checked against the real public-body NIFs P2807900B (Ayuntamiento de Madrid) and Q2826000H (AEAT).
- Because it is a T4 convention, an official publication of the algorithm would take precedence over this entry.
Open questions
Section titled “Open questions”- A 1975 circular we could not find. A circular of the Subsecretaría de Hacienda from 1975 might mention a historical use of “00”. We could not find it. Until it is found, the “CIF starting with
00needs a letter” rule stays in Explicitly NOT implemented: no official list we checked has ever had province code 00. - A written question to AEAT. Option: ask the AEAT in writing to confirm (a) the control type per organisation key (CIF-3) and (b) the control arithmetic (CIF-4), since both come only from an internal technical note and from convention. Not done yet.
Differences from other libraries
Section titled “Differences from other libraries”src/__tests__/stdnum.test.ts compares validate() (default options) with stdnum (the JavaScript port of python-stdnum, stdnum.ES.nif) on about 100,000 generated inputs. Every difference must be one of these decisions; any other difference fails the test.
| Difference | stdnum | This library | Rule |
|---|---|---|---|
| Control of C D F G J U V (and every other key) | Accepts a letter or a digit for every organisation key | Digit for A B C D E F G H J U V, letter for N P Q R S W; cifControl: "lenient" accepts either for C D F G J U V only |
CIF-3 |
Old NIE form X0nnnnnnnL |
Rejected | Valid, canonical XnnnnnnnL |
NIE-3 |
DNI with fewer than 8 digits (1234567L) |
Rejected | Left-padded to 01234567L |
NORM-4 |
| White space other than a space (tab, no-break space, …) | Kept, so the value is invalid | Removed, like spaces | NORM-2 |
Non-ASCII look-alikes (0, ſ, ı, …) |
Folded to ASCII | Kept, so the value is invalid: the canonical form is ASCII upper case | NORM-1 |
ES prefix |
Always stripped | Only with allowVatPrefix, or isValidSpanishVat |
VAT-1 |
| K/L/M NIF with non-digits after the prefix | Not checked (the number is read with parseInt) |
Invalid | KLM-3 |
Placeholders (00000000T, …) |
Valid | Valid by default; rejected with rejectPlaceholders |
POLICY-1 |
Test values (all computed and checked)
Section titled “Test values (all computed and checked)”Valid: 12345678Z · 01234567L · 00000008P · X1234567L · Y1234567X · Z1234567R · X01234567L (old form → X1234567L) · K1234567L · L1234567L · M1234567L · A58818501 · B12345674 · B00123455 · P2807900B · Q2826000H · N1234567D · W1234567D · ES12345678Z (VAT format)
Invalid:
12345678A(wrong letter)12345678I/O/U(letter not in the table)Y1234567L(the prefix changes the letter)X11234567L(only a leading 0 may be dropped)K1234567A(wrong letter)T12345678(no official prefix)123456789(the check character must be a letter)B1234567D(B needs a digit, CIF-3; valid only withcifControl: "lenient")G1234567D(G needs a digit, CIF-3; valid only in lenient mode)Q12345674/N12345674(these keys need a letter)I1234567D/K1234567D/X1234567D(not entity keys; K and X are natural persons with their own rules)B1234567,B123456745(wrong length)B12345675(wrong control)B0012345E(CIF-3: B needs a digit, and the00rule is folklore)
Policy (valid by default, rejected with rejectPlaceholders): 00000000T · 00000001R · 99999999R · X0000000T
Source URLs
Section titled “Source URLs”Live status checked on 2026-09-30 with curl -sI -L. Every source also has an archived copy in the Wayback Machine, so the sources survive URL changes. The archived link is the closest snapshot to 2026-10-02 (the number after /web/ in each link is its date and time, UTC), found through https://archive.org/wayback/available?url=<url>, and each one answered HTTP 200 on 2026-10-02. No capture had to be requested, except for the VIES help text (added on 2026-10-03, when it had no snapshot). The oldest is the CAIB copy of the AEAT D.I.T. note (2020-10-11), which is the only snapshot there is. To check another date, look a page up at https://web.archive.org/web/*/<url>.
The T1 and T2 sources are watched for changes. spec-sources.json, in the repository, records for each BOE text the date of its last update (and of the articles cited here) and whether it was repealed, and for each T2 page a hash of the text of its relevant section. A monthly workflow compares them with the sources and opens an issue labelled spec-change when one changes (CONTRIBUTING.md, “Official sources”).
- RD 1065/2007: https://www.boe.es/buscar/act.php?id=BOE-A-2007-15984 (archived)
- Orden EHA/451/2008 (consolidated): https://www.boe.es/buscar/act.php?id=BOE-A-2008-3580 (archived)
- Orden HAP/5/2016: https://www.boe.es/buscar/doc.php?id=BOE-A-2016-358 (archived)
- Orden INT/2058/2008: https://www.boe.es/buscar/doc.php?id=BOE-A-2008-12050 (archived)
- Orden 7/2/1997 (consolidated): https://www.boe.es/buscar/act.php?id=BOE-A-1997-3364 (archived)
- RD 255/2025 (DNI): https://www.boe.es/buscar/act.php?id=BOE-A-2025-6601 (archived)
- RD 1155/2024 (extranjería): https://www.boe.es/buscar/act.php?id=BOE-A-2024-24099 (archived)
- Decreto 2423/1975 (repealed): https://www.boe.es/buscar/doc.php?id=BOE-A-1975-21698 (archived)
- Interior, check-letter calculation: https://www.interior.gob.es/opencms/es/servicios-al-ciudadano/tramites-y-gestiones/dni/calculo-del-digito-de-control-del-nif-nie/ (live: HTTP 403 to automated requests, probably bot blocking; archived)
- AEAT, NIF of natural persons: https://sede.agenciatributaria.gob.es/Sede/ayuda/manuales-videos-folletos/manuales-practicos/guia-practica-cumplimentacion-modelo-censal-036/anexos/anexo-01-solicitud-nif-documentacion-aportar/informacion-sobre-numero-identificacion-fiscal/composicion-nif/personas-fisicas.html (archived)
- AEAT, NIF of legal persons and entities: https://sede.agenciatributaria.gob.es/Sede/ayuda/manuales-videos-folletos/manuales-practicos/guia-practica-cumplimentacion-modelo-censal-036/anexos/anexo-01-solicitud-nif-documentacion-aportar/informacion-sobre-numero-identificacion-fiscal/composicion-nif/personas-juridicas-entidades.html (archived)
- AEAT D.I.T. note (CAIB copy): https://www.caib.es/sites/civitasmanualsisuport/f/41519 (archived)
- AEAT, checking the NIF of third parties (census check, not format): https://sede.agenciatributaria.gob.es/Sede/ayuda/consultas-informaticas/presentacion-declaraciones-ayuda-tecnica/modelo-030/comprobacion-nif-terceros-efectos-censales.html (archived)
- EU Commission, VIES help (NORM-2; live and archived copy checked on 2026-10-03): https://ec.europa.eu/taxation_customs/vies/#/help. The page loads the quoted text from https://ec.europa.eu/taxation_customs/vies/assets/i18n/en.json (key
help_txt_para00), which is what the archived link captures (archived) - ESNIC placeholder examples (secondary): https://www.openprovider.com/es/blog/comunicado-esnic (archived)