China Resident ID Numbers: Structure, the MOD 11-2 Check Digit, and Why One in Eleven Ends in X
If your platform onboarded even one mainland-China user, you hold 18-character resident identity numbers (身份证号). They look like a birthdate glued to random digits, and the strangest part — a trailing Roman X — causes a disproportionate share of formatting bugs. This guide breaks down the full structure, the check-digit algorithm, and the ways real copies get mangled, all verifiable in the ID card validator, which runs entirely in your browser.
The 18 digits, field by field
| Positions | Field | Notes |
|---|---|---|
| 1–2 | Province | 11 Beijing, 31 Shanghai, 44 Guangdong … plus 71 Taiwan, 81 Hong Kong, 82 Macao in the standard table |
| 3–6 | City / county division | Administrative code; with the province it forms the 6-digit registration division |
| 7–14 | Birthdate | YYYYMMDD, real calendar date (February 30th is rejectable before you ever touch the checksum) |
| 15–17 | Sequence | Assigned within the division on the birthdate; the 17th digit's parity encodes sex — odd male, even female |
| 18 | Check digit | ISO 7064 MOD 11-2 over the first 17; 10 renders as X |
Two cheap rejections most validators skip: the birthdate must be a real date, and the province prefix must exist. Both cost two lines and catch more garbage than the checksum does, because they encode what the garbage got wrong semantically.
The check digit, exactly
Sum each of the first 17 digits times a fixed weight, take the remainder mod 11, and reflect:
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
v = (12 - (Σ digitᵢ × weightᵢ) mod 11) mod 11
check = str(v) if v < 10 else "X"
The weights are powers of 2 mod 11, counted from the right: position 17 gets 2¹, position 16 gets 2², … wrapping with period 10 (2¹⁰ ≡ 1 mod 11). The same primitive-root construction we described for the Unified Social Credit Code — there with base 3 mod 31, here with base 2 mod 11 — and the payoff is identical: every weight is a nonzero residue, so every single-digit substitution is caught, and since adjacent weights always differ (ratio 2 or its inverse, never ≡ 1 mod 11), every adjacent transposition is caught too. That beats Luhn, the mod-10 scheme behind credit cards, which misses transpositions of digit pairs like 0/9.
Worked example, hand-checkable — body 11010119900815001 (Beijing division 110101, born 1990-08-15, sequence 001):
- First four contributions:
1×7 +1×9 +0×10 +1×5 = 21 - Full sum through position 17: 199
199 mod 11 = 1;12 − 1 = 11 → 11 mod 11 = 0; check digit0
The valid number is 110101199008150010. Change the sequence to 002 — same division, same birthdate, legal-looking — and the correct check digit changes; the validator rejects the original 0 with a checksum error. You can reproduce every step, including the birthdate and gender parse, in the validator.
The X, handled properly
The check value 10 has no single decimal digit, so the standard writes X. Three practical rules:
- Uppercase before validating. Lowercase
xin your data is a transcription artifact; the standard's character isX. (This is also why the check character must be stored in a text column — an integer column physically cannot hold it, and the IDs that end in X — about 1 in 11 — silently corrupt on import.) - Full-width
X(U+FF38) is the PDF/IME artifact, same as with USCCs; normalize width (NFKC) first. - Never "fix" a failing check digit by recomputing it — that manufactures a well-formed number that identifies nobody and defeats every later check. Route to manual review.
How real copies get corrupted
| Symptom | Usual cause | Handling |
|---|---|---|
Lowercase x or full-width X | IME / PDF / OCR | Uppercase, NFKC, re-validate |
O for 0 | OCR autocorrect | Map O→0 only during repair attempts, never blanket |
| 15 digits | Legacy pre-1999 numbers (no century in the birthdate, no check digit at all) | These are structurally different — flag for upgrade workflows, don't checksum them |
| Future birthdate or Feb 30 | Typo'd birthdate; validator enforces real calendar dates | Reject before checksum; the error message should say which field |
| Valid checksum, wrong person | Copy-paste from the wrong row / sibling's number | Out of scope for any checksum — see below |
That last row is the honest boundary: a checksum-valid ID is well-formed. It does not prove the number was ever issued, that it belongs to the person who typed it, or that the person is who they claim. That assurance requires government verification services, which the validator deliberately does not attempt — it computes locally, which is also why pasting an ID into it leaks nothing. For test fixtures you want numbers that are checksum-valid and provably synthetic: the China test data generator produces them with correct division shapes and real dates.
FAQ
Do all ID numbers end in X for people born on certain days? No — the trailing X falls out of the arithmetic (check value 10, roughly 1 in 11 numbers regardless of birthdate).
Is the 18-digit format used in Hong Kong / Macao / Taiwan? No. Those regions have their own formats (HK's 8-character, Taiwan's 10-character alphabetic schemes). The standard table reserves province codes 71/81/82 for them, but issued numbers there differ — our guides cover the mainland format only.
Why do some systems store 19 characters?
They are padding or appending a space. The number is exactly 18 characters, and the last one may be X.
Is it safe to paste a real ID into an online validator? Not into one that uploads. Ours computes in your browser and sends nothing — check any validator's Network panel before trusting it with a national ID.
In this series: China identifiers
- Unified Social Credit Code check digit (GB 32100)
- VIN check digit (ISO 3779)
- China license plate format rules
Implementation notes
The reference implementation is one weight table, one check map, and — the part worth copying — a real-calendar birthdate check via a UTC round-trip (new Date(Date.UTC(y, m-1, d)) must land on the same day it started), which catches Feb 30 and friends without a month-length lookup table. The one bug class we saw in the wild: validators that uppercase but don't NFKC-normalize, so full-width digits 1990 fail charset with a confusing message instead of a repairable one. Round-trip tests (generate N syntactically valid numbers, validate all, tamper each digit, assert rejection) pin all of it.
Related Tools
Related Articles
China Unified Social Credit Code (USCC): Structure, Checksum Algorithm, and How to Validate It
The 18-char USCC (GB 32100-2015) decoded for English-language systems: field layout, the 31-character alphabet, the weighted mod-31 checksum with a worked example, and the failure modes that corrupt codes in real data — with a local validator.
How Color-Blind Users See Your Palette: A Six-Type Self-Check and Safe Colors
What does your UI look like to the 8% of men with color vision deficiency? The six types (protanopia/deuteranopia/tritanopia, both anomalies, achromatopsia), matrix-based simulation, a three-pair self-check list, and the WCAG practice of never encoding meaning in color alone.
Markdown to Word: Native Style Mapping and the Inline-Format Downgrade List
Turning a Markdown draft into a deliverable .docx: pandoc CLI vs in-browser local conversion, how headings/tables/code blocks map to native Word styles, which inline formats degrade to plain text, and why 'editable' beats 'looks right'.