IoT Device Pairing via QR Code in Mobile Applications
QR pairing — the fastest way to add an IoT device: the user scans a sticker on the device, the app extracts the identifier and automatically binds it to the account. No serial number entry, no Bluetooth scanning. With proper implementation, the entire process takes 10–15 seconds.
What's Encoded in the QR
The QR code on an IoT device can contain:
- Serial number / Device ID (
SN:ABC12345) - MAC address (
MAC:AA:BB:CC:DD:EE:FF) - Claim token for binding to a specific user
- Matter provisioning code (
MT:Y.K90SO527JA0648G00) - Custom URL with parameters (
https://app.example.com/pair?id=ABC&key=xyz)
Matter uses a numerical Setup Code embedded in the QR during manufacturing — an 11-digit code plus metadata. The format is strictly standardized in the Matter specification (MTR-006).
For custom devices — recommend URL scheme or Base64 JSON. URL scheme allows opening the app directly from the system camera through Deep Link, without opening a browser.
Scanning: ML Kit Barcode
ML Kit Barcode Scanning — the best choice for Android: supports QR, DataMatrix, PDF417, works offline:
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val scanner = BarcodeScanning.getClient(options)
// In CameraX ImageAnalysis
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { qrData ->
viewModel.onQrScanned(qrData)
// Stop camera after first successful scan
cameraProvider.unbindAll()
}
}
Important to stop the camera after the first scan — otherwise ML Kit will be called multiple times for the same QR. Debounce using AtomicBoolean isScanning or Flow.distinctUntilChanged().
On iOS — Vision framework + AVCaptureSession or DataScannerViewController (iOS 16+):
// iOS 16+ — simplest path
let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
isHighlightingEnabled: true
)
scanner.delegate = self
try? scanner.startScanning()
// delegate
func dataScanner(_ dataScanner: DataScannerViewController,
didTapOn item: RecognizedItem) {
if case .barcode(let barcode) = item {
handleQrData(barcode.payloadStringValue ?? "")
}
}
DataScannerViewController on iOS 16+ replaces custom AVCaptureSession implementations — simpler, with built-in highlighting UI.
QR Data Parsing and Validation
QR data needs secure parsing — the user can point the camera at any QR, not just a device:
data class DeviceQrPayload(
val deviceId: String,
val claimToken: String,
val productType: String
)
fun parseQrCode(raw: String): DeviceQrPayload? {
return try {
// URL format: myapp://pair?id=ABC&token=XYZ&type=sensor
val uri = Uri.parse(raw)
if (uri.scheme != "myapp" || uri.host != "pair") return null
DeviceQrPayload(
deviceId = uri.getQueryParameter("id") ?: return null,
claimToken = uri.getQueryParameter("token") ?: return null,
productType = uri.getQueryParameter("type") ?: "unknown"
)
} catch (e: Exception) { null }
}
null on any parsing error — not a crash. Show the user "Unrecognized QR code".
Device Binding: API Call
After QR parsing — make a server request to bind the device to the user's account:
POST /api/devices/claim
{
"device_id": "ABC12345",
"claim_token": "eyJhbGci...",
"device_name": "Kitchen Temperature Sensor"
}
Claim token — one-time, generated during manufacturing, stored in the database. After first successful binding — invalidated. This protects against someone else using a leaked QR.
Matter QR is handled differently: Google Home SDK or Apple HomeKit Framework decrypt the setup payload and perform commissioning themselves. The app doesn't need to make a backend call — Matter platform handles it.
UX After Scanning
Don't show an empty loading screen. As soon as the QR is recognized — show device data (type, serial number, brief description) and an "Add" button. User confirms. Only then — account binding.
This protects against accidental scanning: the user sees exactly what's being added and can cancel.
Implementing QR pairing with account binding: 1–2 weeks. Pricing calculated individually.







