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.
In this guide
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:
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
| Digit | 3 | 5 | 6 | 7 | 4 | 1 | 0 | 8 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| x2 (RTL) | 6 | 5 | 12 | 7 | 8 | 1 | 0 | 8 | 0 | 1 | 4 | 3 | 8 | 5 | 6 |
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.
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)
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 }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Exactly 15 digits after stripping separators and Luhn checksum passes |
| tac | string | First 8 digits — Type Allocation Code (device model identifier) |
| snr | string | Digits 9–14 — Serial Number (manufacturer-assigned, unique per TAC) |
| checkDigit | string | Digit 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
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.