How to Build an IMEI Scanner in HTML5 with Barcode Reading and OCR
An IMEI scanner can be built in a single HTML page with Dynamsoft Capture Vision: the CaptureVisionRouter API either decodes the Code 128 barcode printed on a phone’s label with Dynamsoft Barcode Reader, or reads the printed digits below it with Dynamsoft Label Recognizer, and a Luhn check-digit validator accepts the result only if it forms a valid IMEI. This article walks through the complete implementation in JavaScript, including an image-loading mode so you can verify both scan modes without a camera.
What You’ll Build
A mobile-friendly web page with two scan modes and an image-loading mode:
- Barcode mode: point the camera at the Code 128 barcode on a device label. When a barcode decodes to a valid IMEI, its value is saved immediately.
- Printed-digits mode: point the camera at the text line printed under the barcode, such as
IMEI1:866186087048013. OCR runs continuously until a valid IMEI is extracted, then a dialog asks the user to confirm, correct, or rescan the result — optical character recognition can misread digits, so user intervention keeps the data clean. - Load Image: process a photo from disk (or the bundled sample label) with the same pipeline, which is handy for quick testing without a camera.
Key Takeaways
- One CDN script —
dynamsoft-capture-vision-bundle— replaces three separate SDKs. ItsCaptureVisionRoutercoordinates camera input, barcode decoding, and text recognition through named templates. - IMEI labels usually carry the serial number in a Code 128 barcode. Restricting the
ReadSingleBarcodepreset toBF_CODE_128makes detection faster and prevents unrelated symbologies from matching. - For OCR, the general
RecognizeTextLines_Defaulttemplate beats a digits-only model on real device labels, because labels print prefixes likeIMEI1:that a number-only model mangles into bogus digits. - A sliding 15–17 digit window over the recognized text, verified with the Luhn check digit, reliably extracts the IMEI even when the OCR line contains prefixes or the digits are split across lines.
- Barcode results are trusted automatically, while OCR results are displayed in a Correct/Rescan dialog because recognition errors are possible.
Common Developer Questions
Which barcode format stores the IMEI?
Most retail and carrier labels encode the IMEI in Code 128, which packs all 14 numeric characters plus the Luhn check digit into a compact linear barcode. The sample therefore restricts reading to Code 128 via EnumBarcodeFormat.BF_CODE_128; the OCR mode acts as a fallback when only printed digits exist.
How is a scanned number verified as a valid IMEI?
An IMEI is 15 to 17 digits whose last digit is a check digit computed with the Luhn algorithm. The scanner slides a 15–17 digit window over every recognized string, and only a window that passes both the length rule and the Luhn sum modulo 10 is accepted, so prefixes such as IMEI1: and random partial reads are discarded.
Why do OCR results require confirmation while barcode results do not?
OCR misreads are possible — 8 versus 0, overlapping font strokes, glare on glossy labels — so the app pauses and shows the recognized digits in a Confirm/Rescan dialog. A Code 128 decode is internally self-checked, so a pass of the Luhn validation is sufficient and the result is saved directly.
Why not use a digits-only OCR template for the IMEI line?
Real labels print the line as IMEI1:866186087048013. A digits-only recognition model still tries to read the letters and turns IMEI1: into bogus digits, which corrupts the string before validation ever runs. The general RecognizeTextLines_Default template reads the line as-is, and the digit-window extraction shown in this article recovers the actual IMEI from it.
Prerequisites
- A license key for Dynamsoft Capture Vision: Get a 30-day free trial license
- A local web server or HTTPS hosting — browsers only expose the camera over secure connections
- A device with a rear-facing camera or webcam (not needed for the image-loading mode)
Before diving into the SDK specifics, let us look at what the International Mobile Equipment Identity actually is.
The International Mobile Equipment Identity (IMEI) is a numeric identifier, usually unique, for 3GPP and iDEN mobile phones, as well as some satellite phones. It is usually found printed inside the battery compartment of the phone but can also be displayed on-screen on most phones by entering *#06# MMI Supplementary Service code on the dial pad, or alongside other system information in the settings menu on smartphone operating systems.1 If the phone supports dual sim, then it can have two IMEI numbers.

Since it is a unique value, it is often used in scenarios like managing phones in the inventory and tracking a network device. We can capture the value by reading the barcodes or by recognizing the text.
Step 1: Create the Basic Page
Create a new HTML file with a card containing a mode switcher, a start button, an image picker, a status line, and a result panel:
<div class="card">
<h1>📱 IMEI Scanner</h1>
<p class="subtitle">Scan a barcode or recognize the printed digits</p>
<label class="field-label">Mode</label>
<div class="segmented">
<button id="mode-barcode" class="active" onclick="setMode(0)">📊 Barcode</button>
<button id="mode-text" onclick="setMode(1)">🔢 Printed digits</button>
</div>
<button class="btn btn-primary start-btn" onclick="startScan();" disabled>📷 Start Scan</button>
<button class="btn btn-secondary" onclick="document.getElementById('pick-file').click();">🖼️ Load Image</button>
<input type="file" id="pick-file" accept="image/*" style="display:none;">
<div class="status info"></div>
<div class="result-panel">
<div class="result-label">IMEI RESULT</div>
<div class="result-value"></div>
<button class="copy-btn" onclick="copyResult();">Copy</button>
</div>
</div>
The page has a segmented control to select whether to read barcodes or recognize text, a button to start camera scanning, and a hidden file input for the image-loading mode. The result panel shows the accepted IMEI with a Copy button.

Step 2: Load Dynamsoft Capture Vision and Initialize the SDK
Starting with version 3.x of the Dynamsoft Capture Vision Bundle for JavaScript, barcode reading, label recognition, and camera control ship in one package, so only a single script tag is needed in place of the previous dynamsoft-javascript-barcode, dynamsoft-label-recognizer, and dynamsoft-camera-enhancer bundles:
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-bundle@3.6.3000/dist/dcv.bundle.min.js"></script>
The full-screen video preview lives in a .scanner container. The CameraView component injects its own UI element — including the highlighted scan-region frame, so you do not need to draw one yourself. Only a mount point, a hint line, and a Close button are needed:
<div class="scanner">
<div id="camera-view-container"></div>
<div class="scan-hint"></div>
<button class="close-btn" onclick="stopScan();">✕ Close</button>
</div>
Initialize the SDK once on page load: initialize the license, preload the WASM engines for DBR (barcode reader) and DLR (label recognizer), create the CameraView + CameraEnhancer pair, and create the CaptureVisionRouter, which drives both scan modes:
const LICENSE_KEY = "LICENSE-KEY";
let cvr;
let cameraEnhancer;
window.onload = init;
async function init() {
setStatus("Initializing…", true);
try {
await Dynamsoft.License.LicenseManager.initLicense(LICENSE_KEY, true);
await Dynamsoft.Core.CoreModule.loadWasm(["DBR", "DLR"]);
const cameraView = await Dynamsoft.DCE.CameraView.createInstance();
document.getElementById("camera-view-container").append(cameraView.getUIElement());
// hide the built-in camera and resolution selectors
cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-camera')?.setAttribute('style', 'display:none');
cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-resolution')?.setAttribute('style', 'display:none');
cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(cameraView);
cvr = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
document.getElementsByClassName("start-btn")[0].disabled = "";
setStatus("", true);
} catch (ex) {
console.error(ex);
setStatus("Initialization failed: " + (ex.message || ex), false);
}
}
Note that unlike the legacy Dynamsoft.DBR.BarcodeScanner.license = ... pattern, the license is now initialized through Dynamsoft.License.LicenseManager.initLicense() before anything else runs.
Step 3: Configure the Capture Templates
Capture Vision ships preset capture templates — string names passed to the router such as ReadBarcodes_Default, ReadSingleBarcode, or RecognizeTextLines_Default. This app uses two of them:
const BARCODE_TEMPLATE = "ReadSingleBarcode";
const TEXT_TEMPLATE = "RecognizeTextLines_Default";
Because IMEI labels use Code 128, the ReadSingleBarcode template is tuned to read that symbology only. Fetch its simplified settings, override the format IDs, and push them back:
let settings = await cvr.getSimplifiedSettings(BARCODE_TEMPLATE);
settings.barcodeSettings.barcodeFormatIds = Dynamsoft.DBR.EnumBarcodeFormat.BF_CODE_128;
await cvr.updateSettings(BARCODE_TEMPLATE, settings);
Two details are worth noting. First, the format enum lives under Dynamsoft.DBR in the 3.x bundle — Dynamsoft.Core.EnumBarcodeFormat does not exist and throws Cannot read properties of undefined if used. Second, the OCR side deliberately uses the general RecognizeTextLines_Default template rather than a digits-only one: device labels print the line as IMEI1:866186087048013, and a number-only model mangles the IMEI1: letters into bogus digits, corrupting the string before validation can run. Step 6 shows how the actual IMEI is extracted from the recognized line.
Step 4: Scan the Barcode with the Camera
Two scan regions, measured in percentages of the video frame, limit processing to the strip where the IMEI label sits; the text-mode strip is narrower to exclude neighboring text. The camera’s built-in UI highlights whatever region you set, and the hint line below it tells the user what to aim at:
const REGION_BARCODE = {x:0,y:25,width:100,height:10,isMeasuredInPercentage:true};
const REGION_TEXT = {x:25,y:25,width:50,height:10,isMeasuredInPercentage:true};
function isBarcodeMode() {
return document.getElementById("mode-barcode").classList.contains("active");
}
function setMode(mode) {
document.getElementById("mode-barcode").classList.toggle("active", mode === 0);
document.getElementById("mode-text").classList.toggle("active", mode === 1);
const region = isBarcodeMode() ? REGION_BARCODE : REGION_TEXT;
if (cameraEnhancer) {
cameraEnhancer.setScanRegion(region); // the built-in camera UI shows this region
}
updateScanHint(region);
}
Instead of polling camera frames manually, register a result receiver once. On Start Scan, the router binds the camera enhancer as its input and starts capturing with the template selected by the mode switch:
cvr.addResultReceiver({
onCapturedResultReceived: (result) => { handleResults(result); }
});
async function startScan(){
document.getElementsByClassName("scanner")[0].style.display = "block";
const region = isBarcodeMode() ? REGION_BARCODE : REGION_TEXT;
updateScanHint(region);
cameraEnhancer.setScanRegion(region);
await openCameraIfNeeded();
cvr.setInput(cameraEnhancer);
isScanning = true;
await cvr.startCapturing(isBarcodeMode() ? BARCODE_TEMPLATE : TEXT_TEMPLATE);
}
Every incoming CapturedResult contains typed items — CRIT_BARCODE for barcodes and CRIT_TEXT_LINE for OCR lines. The shared handler filters items by the active mode. A Code 128 result is validated and saved immediately:
async function handleResults(result) {
if (!isScanning || !result || !result.items) {
return;
}
await processItems(result.items);
}
async function processItems(items){
const barcodeMode = isBarcodeMode();
const targetType = barcodeMode
? Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE
: Dynamsoft.Core.EnumCapturedResultItemType.CRIT_TEXT_LINE;
const lines = [];
for (const item of items) {
if (!item || item.type !== targetType) {
continue;
}
if (barcodeMode) {
const imei = extractIMEI(item.text);
if (imei) {
// barcode results are reliable; save the result directly
showResult(imei);
return true;
}
} else {
lines.push(whiteSpacesRemoved(item.text));
// the OCR branch continues in Step 5
}
}
}
function showResult(text){
document.querySelector(".result-panel").classList.add("show");
document.querySelector(".result-value").innerText = text;
stopScan();
}
Closing or finishing a scan stops the router and releases the camera, so nothing keeps running in the background between scans:
async function stopScan(){
await pauseCapturing();
closeModal();
document.getElementsByClassName("scanner")[0].style.display = "none";
closeCameraIfOpen();
}
async function openCameraIfNeeded(){
if (cameraOpened) {
return;
}
const cameras = await cameraEnhancer.getAllCameras();
if (cameras != null && cameras.length > 0) {
await cameraEnhancer.selectCamera(cameras[0]);
} else {
throw new Error("No camera found.");
}
await cameraEnhancer.open();
cameraOpened = true;
}
function closeCameraIfOpen(){
if (cameraOpened) {
cameraEnhancer.close();
cameraOpened = false;
}
}
async function pauseCapturing(){
isScanning = false;
if (cvr) {
try {
await cvr.stopCapturing();
} catch (ex) {
console.error(ex);
}
}
}
Demo video:
Step 5: Recognize the Printed Text
The text branch of processItems collects all recognized lines. A label line like IMEI1:866186087048013 becomes a single string, and digits may also be split across multiple lines, so the code tries every line on its own plus the concatenation of all lines. When an IMEI is extracted, the app pauses capturing and displays a modal showing the digits. Since the OCR may have errors, user intervention is needed — the user can accept (Correct) or restart recognition (Rescan). While nothing valid has been found yet, the hint line shows the current OCR output so the user can see the scanner is working:
// OCR branch of processItems (continued from Step 4)
const sources = [...lines, lines.join("")];
for (const src of sources) {
const imei = extractIMEI(src);
if (imei) {
// OCR may contain errors; ask the user to confirm the result
showConfirmModal(imei);
return true;
}
}
// nothing valid yet; show live feedback so the user knows the OCR is working
if (isScanning && lines.length > 0) {
document.querySelector(".scan-hint").textContent =
"Reading: " + lines[0].slice(0, 32) + (lines[0].length > 32 ? "…" : "");
}
return false;
The modal handlers either save the confirmed digits or resume recognition:
function correct(){
const text = document.getElementById("scan-result").innerText;
closeModal();
showResult(text);
}
async function rescan(){
closeModal();
if (fileMode) {
// let the user pick another image
document.getElementById("pick-file").click();
} else {
cvr.setInput(cameraEnhancer);
isScanning = true;
await cvr.startCapturing(TEXT_TEMPLATE);
}
}
function closeModal(){
document.getElementById("modal").classList.remove("active");
}
The followings are the codes related to the modal.
HTML:
<div class="modal-backdrop" id="modal">
<div class="modal-window">
<p class="modal-title">Is this the correct IMEI?</p>
<pre id="scan-result"></pre>
<div class="modal-actions">
<button class="btn btn-primary" id="correct-btn" onclick="correct()">✓ Correct</button>
<button class="btn btn-secondary" id="rescan-btn" onclick="rescan()">↻ Rescan</button>
</div>
</div>
</div>
CSS:
.modal-backdrop{
display:none; position:fixed; inset:0; background:rgba(10,16,30,.55);
z-index:200; backdrop-filter:blur(2px);
}
.modal-backdrop.active {display:flex; align-items:center; justify-content:center;}
.modal-window {
background:#fff; border-radius:16px; padding:26px 30px; max-width:86%; width:340px;
text-align:center; box-shadow:0 16px 50px rgba(0,0,0,.35);
}
Demo video:
Step 6: Extract and Verify the IMEI
An IMEI is a string of numbers with a length between 15 and 17, and its last digit is a check digit following the Luhn algorithm.
Recognized text rarely consists of just the IMEI — labels add prefixes like IMEI1: and the colon may even be dropped by the OCR, merging the prefix digit into the number. The extractor therefore collects all digits and slides a 15–17 digit window across them, returning the first window that passes the Luhn check:
// Extract a valid IMEI from arbitrary text. Labels often carry prefixes
// such as "IMEI1:" that merge into the digit string, so slide a 15-17
// digit window over all digits and return the first window that passes
// the Luhn check digit validation.
function extractIMEI(str) {
const digits = (String(str).match(/\d+/g) || []).join("");
for (const len of [17, 16, 15]) {
for (let i = 0; i + len <= digits.length; i++) {
const candidate = digits.substring(i, i + len);
if (IMEIValid(candidate)) {
return candidate;
}
}
}
return null;
}
The validation itself is unchanged:
function IMEIValid(imei) {
if (isNaN(imei)) {
return false;
}
if (imei.length >= 15 && imei.length <= 17) {
if (verifyCheckDigit(imei)) {
return true;
}
}
return false;
}
//https://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity#Check_digit_computation
function verifyCheckDigit(imei){
let checkDigit = imei.substring(imei.length-1);
let rest = imei.substring(0, imei.length-1);
let digits = rest.split("");
let sum = 0;
for (let i = 0; i < digits.length; i++) {
const digit = parseInt(digits[i]);
let numbers;
if ((i+1) % 2 === 0) {
let doubled = (digit * 2).toString();
numbers = doubled.split("");
}else{
numbers = [digit];
}
for (let j = 0; j < numbers.length; j++) {
const number = numbers[j];
sum = sum + parseInt(number);
}
}
if ((sum + parseInt(checkDigit)) % 10 === 0) {
return true;
}
return false;
}
Step 7: Test Without a Camera by Loading an Image
The same templates run on a still image through cvr.capture(), so the app can process a photo from disk — or the bundled sample-imei.png label — without ever opening the camera. The recognized items flow through the exact same processItems() validation:
document.getElementById("pick-file").addEventListener("change", function(){
const file = this.files[0];
this.value = ""; // allow selecting the same file again
if (file) {
processFile(file);
}
});
async function processFile(file){
if (isProcessingFile) {
return;
}
isProcessingFile = true;
fileMode = true;
toggleLoading(true);
try {
const src = await readAsDataURL(file);
const template = isBarcodeMode() ? BARCODE_TEMPLATE : TEXT_TEMPLATE;
const result = await cvr.capture(src, template);
const found = await processItems((result && result.items) || []);
if (!found) {
setStatus("No valid IMEI found in the image. Try the other mode or a sharper photo.", false);
}
} finally {
toggleLoading(false);
isProcessingFile = false;
}
}
The bundled sample is fetched straight from the app directory, which makes it a one-click regression test for both modes:
async function loadSampleImage(){
const response = await fetch("sample-imei.png");
processFile(await response.blob());
}
Common Issues & Edge Cases
- The camera never opens. Browsers only grant camera access in secure contexts. Serve the page over
https://or visit it viahttp://localhost; plain HTTP on a LAN IP will silently fail. Cannot read properties of undefined (reading 'BF_CODE_128'). In the 3.x bundle the barcode format enum isDynamsoft.DBR.EnumBarcodeFormat, notDynamsoft.Core.EnumBarcodeFormat.- OCR reads
IMEI1:866186087048013but nothing is accepted. This was the behavior of digits-only OCR templates, which mangle theIMEI1:prefix into bogus digits that fail the Luhn check forever. The generalRecognizeTextLines_Defaulttemplate plus the sliding-window extraction in Step 6 handles these labels. - The scanner seems stuck while text is visible. Watch the hint line — it echoes the current OCR output (
Reading: …). If the digits are blurry or off-angle, the Luhn check keeps rejecting them; tighten the text-mode scan region (REGION_TEXT) around the digits and avoid glare. - Dual-SIM phones. They expose two IMEIs (both shown by
*#06#and usually both printed on the box asIMEI1:/IMEI2:). The sample stops at the first valid read; keep scanning instead of stopping if you need to collect both. - License errors. If initialization fails, the reason is printed in the status area — trial licenses are time-limited, so renew yours if
initLicense()reports an expired key.
Source Code
Get the complete sample project source code on GitHub: https://github.com/tony-xlh/IMEI-scanner/