Skip to content
Constaia

Spanish DNI and NIE check letter: algorithm and code

How the check letter of the Spanish DNI and NIE is computed (modulo 23), with JavaScript, PHP and Python code, edge cases and why a valid letter proves nothing.

By Constaia team5 min read

Also in: Español, Português, Français

In Spain, citizens carry the DNI (Documento Nacional de Identidad): eight digits plus a check letter, such as 12345678Z. Foreign residents get an NIE (Número de Identidad de Extranjero): a leading X, Y or Z, seven digits and a check letter. Both work as the person's tax ID (NIF), so they appear in almost every Spanish form: registrations, invoices, sports licences.

The check letter is the cheapest validation there is: one division and a 23-letter table. Yet in real forms it fails more often than you would expect, because of spaces, hyphens, lowercase letters, leading zeros and NIEs that start with a letter.

Here is the official algorithm, copy-ready code in three languages and the edge cases worth covering. And at the end, an important warning: a correct letter does not prove the document exists.

The official algorithm

The Spanish Ministry of the Interior describes it like this: divide the number by 23 and replace the remainder with a letter from this table.

Remainder01234567891011
LetterTRWAGMYFPDXB
Remainder1213141516171819202122
LetterNJZSQVHLCKE

Written as a single string, the table is TRWAGMYFPDXBNJZSQVHLCKE: the letter is the character at position number % 23.

The official example: 12345678 divided by 23 leaves a remainder of 14, so the letter is Z → 12345678Z.

The NIE

To compute the NIE letter, replace the leading letter with a number and apply the same algorithm (source):

Leading letterReplace with
X0
Y1
Z2

Examples:

  • X1234567 → 01234567 → remainder 19 → L → X1234567L
  • Y1234567 → 11234567 → remainder 10 → X → Y1234567X
  • Z1234567 → 21234567 → remainder 1 → R → Z1234567R

Always divide the full 8-digit number after the substitution. If you divide only the 7 digits of the NIE, X happens to work (a leading 0 changes nothing) but Y and Z give wrong letters.

The code

All three versions do the same thing: normalise the input, check the format (8 digits + letter, or X/Y/Z + 7 digits + letter) and compare the letter.

const LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE";

export function validateNifNie(input) {
  const value = String(input).toUpperCase().replace(/[\s.\-]/g, "");
  const match = /^([XYZ]\d{7}|\d{8})([A-Z])$/.exec(value);
  if (!match) return { valid: false, reason: "format" };
  const [, body, letter] = match;
  const number = Number(body.replace(/^[XYZ]/, (c) => "XYZ".indexOf(c)));
  const expected = LETTERS[number % 23];
  return { valid: letter === expected, expected };
}

validateNifNie("12345678Z"); // { valid: true, expected: "Z" }
validateNifNie("x-1234567-l"); // { valid: true, expected: "L" }
validateNifNie("12345678A"); // { valid: false, expected: "Z" }

We tested all three snippets with the examples in this article.

Edge cases that do show up in forms

Lowercase, spaces, dots and hyphens

People type 12.345.678-z, 12345678 z or x1234567l. Normalise before validating: uppercase and strip separators. Then store the normalised form (12345678Z), not what the user typed.

Leading zeros

A DNI can start with zero. The trouble starts when the value passes through a spreadsheet or a numeric column: 01234567L becomes 1234567L and the 8-digit format check fails. The letter does not change (leading zeros do not affect the remainder), so you can pad with zeros up to 8 digits before validating:

const padded = value.replace(/^(\d{1,7})([A-Z])$/, (_, n, l) => n.padStart(8, "0") + l);

Only do this if you know the source drops zeros; otherwise it is better to reject and ask for the full number.

Missing letter

Many legacy forms store only the number. You can compute the letter to display it, but do not "complete" it silently: if the user mistyped a digit, you will be manufacturing a valid NIF that is not theirs.

Letters that are not in the table

The table does not contain I, Ñ, O or U. If one of them arrives in the check position, the format is fine but the letter will never match: validation returns false, as expected.

Other tax IDs: CIF and special NIFs

  • What used to be called the CIF, now the NIF of legal entities, has a different structure: under Order EHA/451/2008, a letter for the legal form (A for public limited companies, B for private limited companies, G for associations…), seven digits and a check character. Its check character is not computed with the DNI table.
  • Foreign individuals without an NIE who need a tax ID receive one starting with M (Royal Decree 1065/2007, Art. 20).

The code in this article covers DNI and NIE. To validate CIFs and special NIFs too, try our free tool, which detects the type automatically.

What the letter does not tell you

The check letter exists to catch typing errors: one wrong digit or two swapped digits. It is not a security mechanism:

  • Anyone can compute the letter for any number. With a public algorithm, generating a "valid" NIF is trivial.
  • A correct letter does not tell you the number exists, that it belongs to the person filling in the form, or that the document is still valid.

So the letter is a first filter (useful, cheap, instant in the form itself), not an identity check.

How Constaia does it

In Constaia the letter is one of several deterministic checks. When you analyse a DNI or NIE, the response includes the nif_check_digit check alongside others such as mrz_checksums (MRZ check digits) and mrz_matches_visual (the MRZ matches the printed data). With checks.holder.document_number you also check that the number read from the document is the one the user typed in the form.

curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer ck_test_..." \
  -F file=@nie.jpg \
  -F 'options={"expect":["es_dni","es_nie"],"checks":{"not_expired":true,"holder":{"document_number":"X1234567L"}}}'

With a ck_test_… key and the file nie.jpg you get a simulated response without spending credits (test mode). Still, remember: neither the letter nor the MRZ proves a document is authentic, and Constaia is not a biometric KYC system.

The NIF/NIE, MRZ and IBAN validators are part of the open source package @constaia/validators. More in Checks.

Summary

  • Letter = TRWAGMYFPDXBNJZSQVHLCKE[number % 23].
  • For the NIE, replace X/Y/Z with 0/1/2 before dividing.
  • Normalise (uppercase, no separators) and watch out for leading zeros.
  • A correct letter only rules out typing errors.

To check a single number, use the free Spanish NIF, NIE and CIF validator. To validate full documents, create a free account with 250 credits a month.

Sources

  1. 01Spanish Ministry of the Interior — NIF/NIE check digit calculation (Spanish)
  2. 02Directorate General for Gambling Regulation — NIF/NIE check digit calculation
  3. 03Order EHA/451/2008 (tax ID of legal entities), BOE (Spanish)
  4. 04Royal Decree 1065/2007, Article 20 (tax ID of foreign individuals), BOE (Spanish)