Guide · Python · Validation

IMEI Validation in Python — 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 Python 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
x2 (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

# luhn_imei.py — validate an IMEI using the Luhn algorithm
import re


def validate_imei(raw: str) -> bool:
    """Validate an IMEI using the Luhn mod-10 algorithm.

    Strips spaces, hyphens, and dots (common separators in printed IMEIs).
    Returns True if the IMEI has exactly 15 digits and passes Luhn.
    """
    # Strip common separators
    digits = re.sub(r"[\s\-.]+", "", raw)

    if not re.fullmatch(r"\d{15}", digits):
        return False  # must be exactly 15 digits

    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2 == 1:  # double every second digit from the right
            d *= 2
            if d > 9:
                d -= 9
        total += d

    return total % 10 == 0


print(validate_imei("356741080123456"))  # True  ✓
print(validate_imei("356741080123457"))  # False ✗ — one digit off
print(validate_imei("12345678901234"))   # False ✗ — 14 digits

4. Why a regex is not enough

15 digits is necessary but not sufficient

The most common IMEI "validator" is r"^\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. Python code example

Using the requests library — the de facto standard for HTTP in Python. Install it with pip install requests.

# imei_validator.py
import os
import requests

API_KEY = os.environ["ISVALID_API_KEY"]
BASE_URL = "https://api.isvalid.dev"


def validate_imei(imei: str) -> dict:
    """Validate an IMEI using the IsValid API.

    Args:
        imei: IMEI in any format (spaces, hyphens, dots handled automatically).

    Returns:
        Validation result as a dictionary.

    Raises:
        requests.HTTPError: If the API returns a non-2xx status.
    """
    response = requests.get(
        f"{BASE_URL}/v0/imei",
        params={"value": imei},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    response.raise_for_status()
    return response.json()


# ── Example usage ────────────────────────────────────────────────────────────

result = validate_imei("35-674108-012345-6")

if not result["valid"]:
    print("Invalid IMEI")
else:
    print("Valid IMEI")
    print(f"TAC (device model): {result['tac']}")      # → "35674108"
    print(f"SNR (serial): {result['snr']}")              # → "012345"
    print(f"Check digit: {result['checkDigit']}")        # → "6"

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

# app.py (Flask)
import re
from flask import Flask, request, jsonify

app = Flask(__name__)


@app.post("/devices/register")
def register_device():
    data = request.get_json()
    imei = data.get("imei", "")

    try:
        imei_check = validate_imei(imei)
    except requests.RequestException:
        return jsonify(error="IMEI validation service unavailable"), 502

    if not imei_check["valid"]:
        return jsonify(error="Invalid IMEI number"), 400

    # Store normalised — TAC identifies the device model family
    device = db.devices.create(
        imei=re.sub(r"[\s\-.]+", "", imei),  # store without separators
        tac=imei_check["tac"],
        snr=imei_check["snr"],
        **{k: v for k, v in data.items() if k != "imei"},
    )

    return jsonify(success=True, device_id=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 error collection
import csv


def register_devices(csv_path: str) -> dict:
    valid_devices = []
    invalid_imeis = []

    with open(csv_path) as f:
        for row in csv.DictReader(f):
            try:
                check = validate_imei(row["imei"])
            except Exception:
                invalid_imeis.append(row["imei"])
                continue

            if not check["valid"]:
                invalid_imeis.append(row["imei"])
            else:
                valid_devices.append({
                    **row,
                    "tac": check["tac"],
                    "snr": check["snr"],
                })

    return {"valid": valid_devices, "invalid": invalid_imeis}

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: f"{tac[:2]}-{tac[2:]}-{snr}-{check_digit}". 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.