Guide · Node.js · REST API

IMEI Validation in Node.js — Luhn Algorithm for Device IDs

Every mobile device has an IMEI. Validating it correctly requires more than a length check — here's how the Luhn algorithm applies to device identifiers, what TAC and SNR mean, and how to use it in a Node.js application.

1. What is an IMEI?

An IMEI (International Mobile Equipment Identity) is a 15-digit number that uniquely identifies a mobile device on a cellular network. Defined by GSMA and standardised in 3GPP TS 23.003, IMEIs are assigned at the factory and stored in a phone's firmware.

Carriers use IMEIs to block stolen devices from accessing networks. Device management platforms (MDM) use them to inventory assets. E-commerce and insurance platforms use them to verify device identity before processing trade-ins or claims.

You can find an IMEI by dialling *#06# on any mobile device, in Settings, or on the original packaging.


2. IMEI anatomy — TAC, SNR, and check digit

Every IMEI has exactly 15 digits, structured as three components:

35 674108 0123456
35674108 = TAC (8 digits)012345 = SNR (6 digits)6 = check digit

TAC — Type Allocation Code (digits 1–8)

Identifies the device model and manufacturer. The TAC is allocated by GSMA and is the same for all units of the same device model. For example, all iPhone 15 Pro Max units share the same TAC prefix. TAC lookups can tell you the device brand, model, and market segment — though public TAC databases are incomplete.

SNR — Serial Number (digits 9–14)

A 6-digit manufacturer-assigned serial number that, combined with the TAC, makes each device unique. The SNR is sequential within a TAC — meaning the first device off the production line for a given model gets SNR 000001, the next gets 000002, and so on.

Check digit (digit 15)

A single Luhn checksum digit that allows detection of common transcription errors — the same algorithm used for credit card numbers. It is computed from the first 14 digits and must match for the IMEI to be valid.


3. Luhn mod-10 applied to IMEI

The Luhn algorithm (mod-10) is identical for IMEIs and credit cards. Starting from the second-to-last digit and moving left, double every second digit. Subtract 9 from any result greater than 9. Sum all digits. If the total is divisible by 10, the number is valid.

Let's walk through a known-valid IMEI: 356741080123456

Step 1 — Double every 2nd digit from the right

Digit356741080123456
×2 (RTL)6512781080143856

Blue = positions doubled (odd positions from right)

Step 2 — Subtract 9 from doubled results > 9

12 → 12 − 9 = 3 (digit 6, value 6 → doubled 12). All other doubled values are ≤ 9.

Step 3 — Sum all digits and check mod 10

6 + 5 + 3 + 7 + 8 + 1 + 0 + 8 + 0 + 1 + 4 + 3 + 8 + 5 + 6 = 70

70 % 10 = 0 ✓ — valid IMEI

// luhnImei.js — validate an IMEI using the Luhn algorithm
function validateImei(raw) {
  // Strip spaces, hyphens, and dots (common separators in printed IMEIs)
  const digits = raw.replace(/[\s\-.]/g, '');

  if (!/^\d{15}$/.test(digits)) return false; // must be exactly 15 digits

  let sum = 0;
  let shouldDouble = false;

  for (let i = digits.length - 1; i >= 0; i--) {
    let d = parseInt(digits[i], 10);

    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }

    sum += d;
    shouldDouble = !shouldDouble;
  }

  return sum % 10 === 0;
}

console.log(validateImei('356741080123456')); // true  ✓
console.log(validateImei('356741080123457')); // false ✗ — one digit off
console.log(validateImei('12345678901234'));  // false ✗ — 14 digits

4. Why a regex is not enough

15 digits is necessary but not sufficient

The most common IMEI "validator" is /^\d{15}$/. This accepts sequences like 000000000000000 or 123456789012345 — both 15 digits, neither a real IMEI. Without the Luhn check, you will accept millions of invalid numbers.

IMEI vs IMEI/SV

Some systems return an IMEI/SV (Software Version) — a 16-digit variant where the last two digits indicate the software version rather than the Luhn check digit. Applying the Luhn check to an IMEI/SV will give false negatives. Know which format your data source provides.

// 15-digit IMEI — Luhn check applies
'356741080123456'

// 16-digit IMEI/SV — Luhn check does NOT apply to the full string
'3567410801234560'  // last 2 digits = software version, not check digit

Input format varies

IMEIs appear in many formats depending on the source: 356741080123456 (raw), 35-674108-012345-6 (GSMA display format), or with spaces. All represent the same device. Your validator must normalise input before checking.


5. The production-ready solution

The IsValid IMEI API handles input normalisation, exact-length validation, and the Luhn checksum in a single GET request. The response includes the parsed TAC, SNR, and check digit — useful for logging and device classification.

Luhn
Algorithm
mod-10, same as credit cards
<15ms
Response time
pure algorithmic check
100/day
Free tier
no credit card

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: IMEI Validation API docs →


6. Node.js code example

Using the native fetch API (Node 18+). No dependencies required.

// imeiValidator.js
const API_KEY = process.env.ISVALID_API_KEY;
const BASE_URL = 'https://api.isvalid.dev';

/**
 * Validate an IMEI using the IsValid API.
 *
 * @param {string} imei - IMEI in any format (spaces, hyphens, dots handled automatically)
 * @returns {Promise<{ valid: boolean, tac?: string, snr?: string, checkDigit?: string }>}
 */
async function validateImei(imei) {
  const params = new URLSearchParams({ value: imei });

  const response = await fetch(`${BASE_URL}/v0/imei?${params}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(`IMEI API error ${response.status}: ${error.message ?? 'unknown'}`);
  }

  return response.json();
}

// ── Example usage ────────────────────────────────────────────────────────────

const result = await validateImei('35-674108-012345-6');

if (!result.valid) {
  console.log('Invalid IMEI');
} else {
  console.log('Valid IMEI');
  console.log('TAC (device model):', result.tac);    // → "35674108"
  console.log('SNR (serial):', result.snr);           // → "012345"
  console.log('Check digit:', result.checkDigit);     // → "6"
}

In a device trade-in platform or MDM registration flow:

// routes/device.js (Express)
app.post('/devices/register', async (req, res) => {
  const { imei, ...deviceInfo } = req.body;

  let imeiCheck;
  try {
    imeiCheck = await validateImei(imei);
  } catch {
    return res.status(502).json({ error: 'IMEI validation service unavailable' });
  }

  if (!imeiCheck.valid) {
    return res.status(400).json({ error: 'Invalid IMEI number' });
  }

  // Store normalised — TAC identifies the device model family
  const device = await db.devices.create({
    imei: imei.replace(/[\s\-.]/g, ''), // store without separators
    tac: imeiCheck.tac,
    snr: imeiCheck.snr,
    ...deviceInfo,
  });

  res.json({ success: true, deviceId: device.id });
});
Store the tac separately in your database. When you build a TAC-to-model lookup (or integrate a commercial TAC database), you can retroactively enrich all registered devices with model and brand information without re-parsing the full IMEI.

7. cURL example

Validate a raw 15-digit IMEI:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/imei?value=356741080123456"

GSMA display format with hyphens:

curl -G -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "value=35-674108-012345-6" \
  "https://api.isvalid.dev/v0/imei"

Invalid IMEI (bad check digit):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/imei?value=356741080123457"

8. Understanding the response

Valid IMEI:

{
  "valid": true,
  "tac": "35674108",
  "snr": "012345",
  "checkDigit": "6"
}

Invalid IMEI (bad checksum or wrong length):

{
  "valid": false
}
FieldTypeDescription
validbooleanExactly 15 digits after stripping separators and Luhn checksum passes
tacstringFirst 8 digits — Type Allocation Code (device model identifier)
snrstringDigits 9–14 — Serial Number (manufacturer-assigned, unique per TAC)
checkDigitstringDigit 15 — the Luhn check digit

Fields tac, snr, and checkDigit are only present when valid is true.


9. Use cases and edge cases

Device trade-in and insurance claims

IMEI validation is the first line of defence against fraudulent trade-ins. After validating the format, consider checking the IMEI against a blacklist (GSMA Device Check, carrier blacklists) to confirm the device has not been reported stolen before processing a trade-in or insurance claim.

IoT device registration

Cellular IoT modules also carry IMEIs. When registering devices in bulk from a CSV import, validate each IMEI before inserting into your database to avoid corrupt records.

// Bulk validation with early rejection
async function registerDevices(rows) {
  const results = await Promise.allSettled(
    rows.map(async (row) => {
      const check = await validateImei(row.imei);
      if (!check.valid) throw new Error(`Invalid IMEI: ${row.imei}`);
      return { ...row, tac: check.tac, snr: check.snr };
    })
  );

  const valid = results.filter(r => r.status === 'fulfilled').map(r => r.value);
  const invalid = results.filter(r => r.status === 'rejected').map(r => r.reason.message);

  return { valid, invalid };
}

All-zeros and other test patterns

000000000000000 is a well-known invalid IMEI used in testing (it passes the 15-digit format check but fails Luhn). Other popular test patterns like repeated digits or sequences may also pass Luhn by chance — validate against Luhn, but also consider rejecting known placeholder patterns in your business logic.

Displaying IMEIs to users

The standard human-readable format is groups separated by hyphens: 35-674108-012345-6. You can construct this from the API response: `${tac.slice(0,2)}-${tac.slice(2)}-${snr}-${checkDigit}`. Store the raw 15-digit string in your database and format for display.


Summary

Do not validate IMEIs with a 15-digit regex alone — Luhn is required
Do not apply the Luhn check to 16-digit IMEI/SV numbers
Strip hyphens, spaces, and dots before validating
Store the TAC separately to enable model lookups later
For trade-ins, check against a carrier/GSMA blacklist after format validation
Display IMEIs in the 35-XXXXXX-XXXXXX-X format for user-facing UIs

See also

Validate IMEI numbers instantly

Free tier includes 100 API calls per day. No credit card required. Returns TAC, SNR, and check digit for every valid IMEI.