Guides
Every decoded barcode or QR code is available to your page the moment it's scanned — through a DOM event, a named global function, or a JavaScript callback. Pick whichever fits how the rest of your integration is built. All three fire from the same place, with the same data, whether the scan came from the live camera or the toolbar's upload icon.
result.text (and
result.bytes) is handed only to your own page, through the methods below — it
is never included in the usage-tracking calls this loader makes to the Scanify dashboard. Those calls only
ever carry format and enhanced, plus the session
lifecycle (start/end) — never the scanned data itself, whatever it is. Where that data ends up after your
own handler receives it (e.g. the fetch(...) call in the example below) is
entirely up to your own code and your own backend.
Every capture method hands you the same result object:
| Field | Type | Description |
|---|---|---|
text | string | Decoded text content. |
format | string | Symbology id, e.g. "qrcode", "ean13". |
bytes | Uint8Array | Raw payload bytes, for formats where the raw encoding matters more than the decoded string. |
enhanced | boolean | Whether the low-light/glare enhancement pipeline ran before this decode succeeded. |
timestamp | number | performance.now() when the result was produced. |
position | object | { topLeft, topRight, bottomLeft, bottomRight } corner points in video-pixel coordinates. |
The loader dispatches a jsscanner:scan CustomEvent
on the mounted container (and it bubbles, so listening on document catches
every scanner on the page). This works with the plain button-and-script-tag snippet — no extra markup needed.
document.addEventListener("jsscanner:scan", (event) => {
const { result, token, sessionId } = event.detail;
console.log(result.format, result.text);
// e.g. send it to your own backend
fetch("/my-api/scans", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ format: result.format, text: result.text }),
});
});
event.detail also carries token (your embed token) and sessionId (the tracked session this scan belongs to, once it's ready — see timing note below).
For snippet-only integrations that would rather not add a separate <script>
block: point data-jsscanner-onscan at a function name already in scope.
It's resolved on every scan, not just at mount time, so it's fine to define it after the
snippet, or even change it later.
<button type="button" data-jsscanner-trigger
data-jsscanner-onscan="handleScan">
Open Scanner
</button>
<script src="https://your-app.test/embed/YOUR_TOKEN.js"></script>
<script>
function handleScan(result, meta) {
document.getElementById("sku-input").value = result.text;
}
</script>
If you're already calling mount() or openFullscreen()
yourself, pass onScan directly — useful when the handler needs to close
over local variables instead of reading/writing global state.
document.addEventListener("jsscanner:ready", () => {
document.getElementById("my-button").addEventListener("click", () => {
const scanner = JSScannerEmbed.openFullscreen({
onScan(result, meta) {
console.log("scanned", result.text, "in session", meta.sessionId);
},
});
});
});
Same option on JSScannerEmbed.mount(target, { onScan }) for inline embeds.
Since mount()/openFullscreen() also return the
underlying scanner instance, you can reach for the SDK's own scanner.on("scan", ...)
event too — the three methods above are convenience wrappers around exactly that, added because the
zero-JS button snippet has no other way to reach the scanner instance at all.
sessionId
Opening the scanner starts the camera immediately (see why),
while the tracked session is created in parallel over the network. If a scan happens in that small
window, meta.sessionId (or event.detail.sessionId)
will be null — the scan data itself is never delayed or dropped waiting on
this, only the usage-tracking association can lag a beat behind it.