Matter QR Code Decoder
Decode the commissioning QR code that Matter / CHIP devices ship with. A Matter
device's setup QR encodes a short MT: URI that carries the information a
commissioning app (e.g. Apple Home, Google Home, the Matter app) needs to pair
with the device for the first time. This tool takes that URI and shows you what
is inside.
No build, no install, no package step. Two ways to use it: a browser app and a command-line script that share the same decode logic.
What it decodes
Given an MT: URI, the payload (84 bits, see Data format) is
split into these fields:
| Field | Meaning |
|---|---|
| Version | Encoding version (3 bits) |
| Vendor ID | 16-bit manufacturer identifier, shown as decimal + 0x-hex, with a name looked up from manufacturers.tsv when available |
| Product ID | 16-bit vendor-assigned product identifier, decimal + 0x-hex; for any vendor with a catalog the product ID is also resolved to a name from products.tsv (and, via vendor-equivalents.tsv, an equivalent vendor's catalog) when available |
| Custom Flow | Commissioning flow type: 0 = Standard, 1 = User Intent, 2 = Custom, 3 = Reserved |
| Discovery | How the device can be found: SoftAP / BLE / On IP Network (a bitset — a device may show more than one) |
| Discriminator | 12-bit discriminator |
| Passcode | 27-bit commissioning passcode |
| Pairing Code | The Matter manual pairing code — the PIN a commissioning app (Apple Home, Google Home, the Matter app) asks you to type. Not a raw field: derived from the passcode, discriminator and (for non-standard flows) the vendor/product IDs, then guarded by a Verhoeff check digit. 11 digits for the standard flow, 21 for user-intent/custom |
Components
| File | What it is |
|---|---|
matter-qr-decoder.html |
The browser app. Single self-contained HTML file (Vue 3 + Bootstrap 5.3, loaded from CDNs). |
matter-decode.sh |
A pure-bash CLI port of the same decode logic. No camera scanning. |
manufacturers.tsv |
Tab-separated vendor ID → name table (~900 rows) used to turn a numeric vendor ID into a name. Shared by both tools; optional. |
gen-manufacturers.py |
Python 3 script that regenerates manufacturers.tsv; pulls the upstream manufacturers.xml over the network by default (or a URL/local file/HTML block you pass in). |
products.tsv |
Tab-separated vendor ID → product ID → name table (3 columns) used by matter-decode.sh to turn a product ID into a name, for any vendor that has a catalog (e.g. LIFX, vendor 0x1306 / 4870). Optional. |
vendor-equivalents.tsv |
Tab-separated alias vendor ID → canonical vendor ID table recording companies that are the same after a merger/acquisition (e.g. Feit 5155 = LIFX 4870), so a product resolves under either ID. Hand-edited; optional. |
gen-lifx-products.py |
Python 3 script that (re)generates LIFX's rows in products.tsv, merging them in from LIFX's upstream products.json over the network by default (or a URL/local file); preserves other vendors' rows. |
CLAUDE.md documents the internals for anyone (or Claude Code) working on the
code.
Browser app
Requirements
-
A modern browser with network access (the page loads Vue 3, Bootstrap 5.3, and Bootstrap Icons from CDNs).
-
Camera scanning needs a secure context — serve it over
HTTPSorlocalhost, notfile://:python3 -m http.server 8000 # then open http://localhost:8000/matter-qr-decoder.html
Use it
- Paste an
MT:URI into the Input box — decoding happens live as you type. - Or click Scan QR and point your camera at the code. Scanning stops itself
the moment it reads an
MT:QR.
Two caveats:
- Scan QR is Chromium-only (Chrome / Edge). It uses the
BarcodeDetectorAPI. Firefox and Safari show a "not supported" message — pasting a URI works everywhere, so use that on those browsers. - The Data Encoding Format and Discovery Capabilities Bits sections at the bottom of the page explain the layout shown in the output.
Command line
matter-decode.sh decodes a URI from an argument or from stdin and prints the
same fields as the web UI:
./matter-decode.sh "MT:..."
echo "MT:..." | ./matter-decode.sh
Example output (run the URI through the script to reproduce):
$ echo "MT:O083635783973181337368400343813154179" | ./matter-decode.sh
Version: 0
Vendor ID: 13012 (0x32D4)
Product ID: 50616 (0xC5B8)
Custom Flow: User Intent Commissioning Flow
Discovery: BLE
Discriminator:1723
Passcode: 8126392
Pairing Code: 549080049513012506168
(The vendor ID 0x32D4 isn't in manufacturers.tsv, so no name is printed —
that's expected, not an error.)
The script is pure bash (3.x+). It reads manufacturers.tsv from its own
directory to resolve the vendor name; without that file every field still decodes
and only the vendor name is omitted.
For any vendor that has a catalog in products.tsv (e.g. LIFX, vendor ID
4870 / 0x1306) the script additionally resolves the product ID to a name,
printed on the Product ID line just like the vendor name on the Vendor ID line.
When a vendor has an equivalent in vendor-equivalents.tsv (a company merged
with or acquired — e.g. Feit 5155 = LIFX 4870), the lookup also checks the
equivalent's catalog and the Vendor ID line is annotated, so either company's
device resolves to the shared product name:
$ echo "MT:R5QU6X.D01M3QG5.000" | ./matter-decode.sh
Version: 1
Vendor ID: 4870 (0x1306) Lifi Labs INC. dba LIFX (a.k.a. Feit Electric Company, Inc.)
Product ID: 123 (0x007B) LIFX Mini 3.1 Color US
Both files are optional, like manufacturers.tsv: without products.tsv the
product name is omitted, and without vendor-equivalents.tsv no equivalent is
considered — but every field still decodes.
Data format
The URI is MT: followed by a base38 payload (charset 0-9 A-Z - ., 38
symbols). The payload decodes to bytes, read little-endian, then sliced into
the 84-bit field layout LSB-first:
version(3) | vendor ID(16) | product ID(16) | custom flow(2)
| discovery caps(8) | discriminator(12) | passcode(27)
Base38 decoding is greedy: 5 chars → 3 bytes, 4 → 2, 2 → 1; a length that fits none of those groupings is an error. Both the HTML app and the shell script use this same scheme, which is why the two stay in lockstep.
Vendor name lookup
Both tools map a numeric Vendor ID to a name using
manufacturers.tsv (vendor ID in the first column, name in the second). The
table is derived from upstream
project-chip/connectedhomeip .../zcl/data-model/manufacturers.xml
(Apache-2.0, generated 2022-08-02). Refresh it from upstream rather than
hand-editing individual rows.
To refresh the table, run gen-manufacturers.py (Python 3 standard library
only). By default it pulls the upstream manufacturers.xml over the network
(via curl) and writes the sorted two-column TSV (decimal id \t name) to
manufacturers.tsv:
python3 gen-manufacturers.py
The source is pinned to the same commit the HTML block was generated from, so a
fresh pull reproduces the current manufacturers.tsv exactly. To refresh from a
different upstream revision, pass a source — a URL (a GitHub blob/ URL is
auto-rewritten to its raw.githubusercontent.com form) or a local XML file:
python3 gen-manufacturers.py https://.../manufacturers.xml
python3 gen-manufacturers.py ./manufacturers.xml
No network? Use the embedded block as the source instead:
python3 gen-manufacturers.py matter-qr-decoder.html
The same table is also embedded as a <map> XML block near the end of
matter-qr-decoder.html (marked "do not edit manually"). This script regenerates
only the TSV, so if you refresh the TSV from new upstream XML, also paste that XML
into the HTML's <script type="text/xml"> block to keep the two copies in sync.
Product name lookup
matter-decode.sh also maps a Product ID to a name, for any vendor that has
a catalog. The product map is products.tsv (three columns: vendor ID \t product ID \t name); when the decoded vendor ID — or any of its equivalents, see
below — is present, the product ID is resolved to a name and printed on the
Product ID line. The table is optional — a missing file only drops the product
name, like manufacturers.tsv for the vendor name.
LIFX's rows (vendor 4870) are derived from LIFX's upstream
LIFX/products .../products.json — refresh them from there rather than
hand-editing. One caveat: that file carries only a pid per product, with no
separate Matter/CHIP product-id field, so the lookup assumes LIFX's Matter
product ID equals their pid.
To refresh LIFX's rows, run gen-lifx-products.py (Python 3 standard
library only). By default it pulls LIFX's products.json over the network
(via curl, falling back to urllib) and merges the rows into
products.tsv — replacing LIFX's (vendor 4870) rows in place and leaving any
other vendors' rows untouched:
python3 gen-lifx-products.py
To refresh from a different source, pass a URL or a local .json file:
python3 gen-lifx-products.py https://.../products.json
python3 gen-lifx-products.py ./products.json
Add another vendor the same way: a gen-<vendor>-products.py that parses that
vendor's catalog and merges its rows — keyed under its own vendor ID — into the
same products.tsv.
Vendor equivalences
Some vendor IDs belong to the same company after a merger or acquisition — e.g.
Feit Electric (5155) acquired LIFX (4870). Record such equivalences in
vendor-equivalents.tsv (two columns: alias vendor ID \t canonical vendor ID;
lines starting with # are ignored). When a decoded vendor has an equivalent,
the product lookup also checks the equivalent's catalog and the Vendor ID line is
annotated (a.k.a. <equivalent name>), so a Feit device resolves to a LIFX
product name and vice-versa.
The table is hand-edited and optional: a missing file simply means no equivalent is considered. The canonical (col-2) id should not itself appear in col-1.
License
The vendor mapping is derived from Matter/CHIP's manufacturers.xml, which is
Apache-2.0. The LIFX product mapping is derived from LIFX's products.json.
Vendor equivalences (vendor-equivalents.tsv) are maintained locally. The app
itself is a small single-file utility.