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 isvalid-sdk Python SDK or the popular requests library. Install either with pip install isvalid-sdk or pip install requests.
# imei_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) result = iv.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
Python integration notes
Pydantic V2's Annotatedtype with AfterValidatorintegrates IMEI validation directly into your data models. Define the annotated type once and use it in FastAPI request bodies, SQLModel ORM models, and Pydantic dataclasses. The validator raises aValueError with the API's error message, which Pydantic automatically surfaces in validation error details — no extra error-mapping code needed.
FastAPI and Django integration
In FastAPI, share a single httpx.AsyncClientvia Depends() created during the app lifespan. In Django, implement IMEI validation in a model'sclean() method or a form field's validate(). For Django REST Framework, overrideto_internal_value() in a custom serializer field and raiseserializers.ValidationError.
Bulk validation of IMEI values — during an import, a migration, or a compliance scan — is most efficient withasyncio.gather()bounded by an asyncio.Semaphore. Process in chunks of 50–100 items, writing each chunk's results to the database before moving on to keep memory usage bounded and the job resumable.
Pre-process IMEI values before calling the API:.strip() trims whitespace, re.sub(r'\s+', '', value)removes internal spaces, andunicodedata.normalize('NFC', value)resolves Unicode normalisation issues from multiple source systems.
- Use
python-dotenvto loadISVALID_API_KEYand assert it at startup - Test async validation functions with
pytest-asyncioand mock HTTP calls withrespx - Annotate validated fields as
NewType('Imei', str)for self-documenting function signatures - Use
functools.lru_cacheon the synchronous validation wrapper for scripts that check the same values repeatedly
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.