Scanify Docs

Guides

Capture scan data

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.

Decoded content never reaches our servers. 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.

The scan result

Every capture method hands you the same result object:

FieldTypeDescription
textstringDecoded text content.
formatstringSymbology id, e.g. "qrcode", "ean13".
bytesUint8ArrayRaw payload bytes, for formats where the raw encoding matters more than the decoded string.
enhancedbooleanWhether the low-light/glare enhancement pipeline ran before this decode succeeded.
timestampnumberperformance.now() when the result was produced.
positionobject{ topLeft, topRight, bottomLeft, bottomRight } corner points in video-pixel coordinates.

Method 1 — listen for the DOM event

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).

Method 2 — name a global function

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>
Note: if the named function doesn't exist (yet, or at all), the loader logs a console error instead of throwing — a scan is never dropped silently, but it's also never allowed to break the scanner itself.

Method 3 — JavaScript callback

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.

A timing note on 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.