Developers & agents
Building a decode-verify loop
A decode-verify loop renders the QR code exactly as it will ship, rasterises it to a bitmap, decodes the bitmap with a real decoder such as zxing, and compares the result against the input. If they differ, the code does not ship. UseQR exposes this as GET /api/v1/verify, returning scannable, decoded, matchesInput, contrast and issues.
The loop
Why you should verify makes the case; this page is the implementation. Four steps, in order, with no shortcuts:
- Render the code with every styling decision applied — module shape, colours, gradient, logo, quiet zone. Verifying an unstyled version proves nothing about the styled one.
- Rasterise to a bitmap at a realistic size. Decoders read pixels, not SVG paths. 512 px is a reasonable default; smaller is a stricter test.
- Decode with a real decoder. Not your own matrix reader — an independent engine like ZXing that behaves like the phones in the field.
- Compare the decoded string against the input, byte for byte. "Something decoded" is not a pass; the right thing decoded is.
In code, using the same libraries UseQR uses internally:
import { readBarcodes } from "zxing-wasm/reader";
async function verify(svg, rasterise, expected) {
const png = await rasterise(svg, 512); // e.g. resvg or canvas
const results = await readBarcodes(png, { formats: ["QRCode"], tryHarder: true });
const decoded = results[0]?.text ?? null;
return { scannable: decoded !== null, matchesInput: decoded === expected };
}
Beyond pass/fail: score the margin
A bare decode is a weak signal — a code can pass at the edge of readability and die at the first drop of ink spread. The verify report should carry margin indicators. UseQR's report shape:
{
"scannable": true,
"decoded": "https://example.com",
"matchesInput": true,
"version": 2,
"ecLevel": "M",
"contrast": 12.6,
"issues": []
}
The margin checks behind issues, with their thresholds:
- Contrast — the foreground/background contrast ratio; below 2.5:1 many scanners struggle, so it is flagged. (Print wants far more: ≥ 40% luminance difference.)
- Logo coverage — a logo above 25% coverage without error correction level H is flagged before it fails in the field.
- Mismatch — decoded text differing from input is reported separately from a failed decode, because it means an encoder bug, not a styling problem.
Damage tolerance you can probe by decoding at descending raster sizes or after adding synthetic noise — a code that survives a 256 px rasterisation has margin that a barely-passing 1024 px code does not.
As a service
If you would rather not assemble the pieces:
curl -sG "https://useqr.app/api/v1/verify" \
--data-urlencode "data=https://example.com" \
--data "color=555555" | jq .
Keyless, and it accepts every styling parameter the
generate endpoint does, so it verifies the
exact code you would ship. The MCP server exposes the same loop as a qr_verify tool,
which lets an AI agent prove a
styled code scans before presenting it.
In CI
The loop earns its keep as a gate. UseQR's own pipeline decode-verifies every combination of module style, eye style, gradient and EC level on every build — a styling regression is a failed build, not a support ticket. The single-code version is two lines in any pipeline:
curl -sG "https://useqr.app/api/v1/verify" --data-urlencode "data=${URL}" \
| jq -e '.scannable and .matchesInput' > /dev/null || exit 1
More patterns — matrix snapshots, fixture decoding — in QR codes in CI. And remember the loop's honest limit: it proves the digital artwork decodes. Ink, laminate, glare and camera angle still need a printed proof.
FAQ
What is a decode-verify loop?
Render the QR code with all styling applied, rasterise it to a bitmap, decode the bitmap with a real decoder, and compare the decoded text with the input. Any mismatch or failed decode blocks the code from shipping.
Why compare the decoded text with the input?
Because "a code decoded" and "the right code decoded" are different claims. A payload-building bug produces a perfectly scannable code containing the wrong data — comparison catches it, bare decoding does not.
What contrast ratio does a QR code need to decode?
UseQR's verify flags anything below 2.5:1 as at-risk for digital scanning, but print needs much more margin — aim for at least a 40% luminance difference with dark modules on a light background.
Is there an API that verifies a QR code scans?
Yes. GET https://useqr.app/api/v1/verify?data=hello renders the requested code server-side, decodes it back with zxing, and returns a JSON report with scannable, matchesInput, contrast and a list of concrete issues. No API key needed.
Try it — free, no signup
Related
- Why you should verify that a QR code decodes — Rendering a QR code proves nothing about whether it scans. Styling, colour, logos and print all consume error-correction budget invisibly. The only…
- QR codes in CI and automated testing — How to test QR codes in a pipeline: decode assertions, snapshot the matrix rather than the PNG, verify endpoints as gates, and E2E download-and-decode round trips.
- Decode a QR code programmatically — Read QR codes from images in code — POST bytes or a URL to the keyless decode API, or decode locally with zxing-cpp, zbar or pyzbar. Snippets included.
- A free QR code API with no key — UseQR's REST API needs no signup, no API key and no SDK. GET /api/v1/qr?data=hello returns a PNG. The shortest form is /q/hello.png, which drops straight…