Every now and then a project needs a realistic-looking IMEI — seeding a test database, mocking a device-management API, populating a form in QA. You can grab one off a phone's dial pad with *#06#, but that's a real identifier tied to a real handset, and pasting it into a repo feels wrong. What you actually want is a number that looks like an IMEI, passes the same checksum a real one does, and isn't attached to anyone's actual device.
That's the whole reason I built the IMEI Generator on spanners.dev. Before I get to the tool, though, it helps to know what those 15 digits actually encode — because once you do, generating a valid one is a surprisingly small amount of code.
What is an IMEI?
An IMEI (International Mobile Equipment Identity) is a 15-digit number that uniquely identifies a mobile handset. It's burned into the device at manufacture and used by carrier networks to block stolen phones, among other things. The structure is fixed by the ITU spec:
| Positions | Length | Field | Meaning |
|---|---|---|---|
| 1–8 | 8 | TAC (Type Allocation Code) | Identifies the model and manufacturer |
| 9–14 | 6 | Serial number | Unique per device within that TAC |
| 15 | 1 | Check digit | Luhn checksum over the first 14 digits |
So 35 123456 789012 4 means TAC 35123456, serial 789012, and a check digit of 4 that ties the whole thing together.
There's also a 16-digit variant called IMEISV (IMEI Software Version). It keeps the same TAC and serial but replaces the check digit with a 2-digit SVN (Software Version Number). Because the SVN changes with firmware updates, IMEISVs don't carry a Luhn check digit — the last two digits are version info, not error detection.
The Luhn check digit
The 15th digit of an IMEI is computed with the Luhn algorithm — the same one used on credit card numbers. The idea is a single-digit checksum that catches any single-digit typo and most transpositions.
The rules, applied to the first 14 digits:
- Starting from the left, double every digit in an odd position (1-indexed: 2nd, 4th, 6th…).
- If a doubled value exceeds 9, subtract 9 (equivalent to summing its digits).
- Sum all the resulting digits.
- The check digit is
(10 - (sum % 10)) % 10— the extra modulo handles the case wheresumis already a multiple of 10, giving a check digit of0rather than10.
Here's the actual implementation from the tool, verbatim:
// Luhn check digit for the first 14 digits of an IMEI.
const luhnCheckDigit = (digits14: string): number => {
let sum = 0;
for (let i = 0; i < digits14.length; i += 1) {
let n = Number.parseInt(digits14[i]!, 10);
if (i % 2 === 1) {
n *= 2;
if (n > 9) n -= 9;
}
sum += n;
}
return (10 - (sum % 10)) % 10;
};
Note the i % 2 === 1 check — because the loop is 0-indexed, that picks out the 2nd, 4th, 6th positions, which are the "odd" positions when you count from 1. Easy to get backwards if you're translating the spec naively.
Validation is just the inverse: take the first 14 digits, recompute the check digit, and compare it to the 15th.
const validateLuhn = (digits15: string): boolean => {
if (!/^\d{15}$/.test(digits15)) return false;
const check = Number.parseInt(digits15[14]!, 10);
return luhnCheckDigit(digits15.slice(0, 14)) === check;
};
IMEISV skips this entirely — a 16-digit input is treated as structurally valid as long as it's all digits, since there's no check digit to verify.
The tool
All of the above lives in a single component at spanners.dev/tools/imei-generator. It has two tabs: Generate and Validate.
Generate
You pick a format (IMEI or IMEISV), a quantity (1–100), and a TAC source:
- Manufacturer presets — a small catalog of real TAC prefixes for popular handsets (iPhone 15, Galaxy S24, Pixel 8, Nokia G22, etc.). These are publicly known ranges; they make generated numbers look realistic without being anyone's actual IMEI.
- Custom TAC — enter any 8-digit TAC. Shorter values get zero-padded.
- Fully random — random TAC, random serial, computed check digit.
Generation is three lines of glue around the Luhn function:
const generateImei = (tac: string): string => {
const serial = randomDigits(6);
const head = `${tac}${serial}`;
const check = luhnCheckDigit(head);
return `${head}${check}`;
};
randomDigits is just Math.random() zero-padded to the requested length — fine for fake data, not for anything that needs cryptographic randomness. The page says as much.
Each result has its own copy button, there's a "copy all" for the batch, and a toggle to display numbers in the AA-BBBBBB-CCCCCC-D dashed format.
Validate
Paste one or more IMEIs (one per line) and it tells you, per line:
- Whether the Luhn check passes (for 15-digit inputs)
- The parsed TAC, serial, and check digit (or SVN for IMEISV)
- Which device the TAC corresponds to, if it's in the catalog
Input is sanitised first — spaces and dashes are stripped — so 35-123456-789012-4 validates the same as 351234567890124. A 16-digit line is assumed to be IMEISV and checked structurally only.
Implementation notes
A few things worth flagging if you're building something similar:
- Single-file architecture. The whole tool — logic, types, catalog, UI — is one ~700-line route file. The spanners.dev codebase does this deliberately: each tool is self-contained and reuses a shared UI kit (
Button,Card,ToolHeader,CopyButton). No per-tool hooks or utils to chase across the tree. - Clipboard fallback.
navigator.clipboardneeds a secure context, so the sharedcopyToClipboardhelper falls back to a hidden textarea +document.execCommand("copy")for older browsers. Still worth having — not everything that opens the tool is on HTTPS. - No tests yet. Other tools in the repo have Vitest coverage; this one doesn't. The Luhn function is the obvious candidate — it's pure, deterministic, and exactly the kind of thing where a typo in the modulo logic silently produces wrong check digits.
The takeaway
IMEEs look opaque until you realise they're just an 8-digit model code, a 6-digit serial, and a single Luhn check digit stapled on the end. Once you know that, generating a valid one is a 12-line function and validating one is even less.
The tool wraps that up with presets, batch generation, and a validator that also identifies the handset — all client-side, nothing leaves the browser. Next time you need a realistic IMEI for a test fixture, grab one there rather than off a real phone.