Integrate with DUST Go
DUST Go is an embedded mobile browser: it loads your web app in a WebView and exposes the device’s DUST scanning hardware to the page through a small JavaScript bridge, @dustid/dust-go-connect. You build and host an ordinary web app; DUST Go supplies the camera, optical accessories, and capture pipeline — no native toolchain required.
This tutorial walks through detecting DUST Go, capturing a scan, and resolving it against APID.
How it fits together
Section titled “How it fits together”- A user opens your web app inside DUST Go (via an app link).
- Your page imports
@dustid/dust-go-connect; the library detects the DUST Go bridge and exposes aconnector. - Your page calls
scanAsync(). DUST Go opens the native scanner over your page. - On capture, DUST Go delivers a scan event back to your page: the payload carries the capture data and metadata.
- Your app forwards the capture to APID (
/api/v1/tags/identify,/bind, or/verify) to resolve it.
Install
Section titled “Install”npm install @dustid/dust-go-connectThe package is dependency-free, MIT-licensed, and ships TypeScript types.
Detect DUST Go
Section titled “Detect DUST Go”The connector export is undefined when your page is not running inside DUST Go, so the same build of your app can serve regular browsers and DUST Go:
import { connector } from "@dustid/dust-go-connect";
export const insideDustGo = Boolean(connector);Two things to know:
- Import timing. Detection happens at module-import time, in the browser. If you server-render, gate any connector usage on hydration.
- Server-side detection. Recent DUST Go builds also tag the WebView User-Agent with
DustGo/<version> (<app id>), so your server can detect the app before any JavaScript runs. Treat this as a hint, not a security boundary — anyone can spoof a User-Agent.
Capture a scan
Section titled “Capture a scan”The simplest path is the promise API — present the scanner and await one capture:
import { scanAsync } from "@dustid/dust-go-connect";
const payload = await scanAsync();// payload: { type: 'DUST' | 'QR' | 'BARCODE' | 'DATA_MATRIX' | 'NFC',// data: string, metadata?: ScanMetadata }scanAsync() returns a Promise<ScanPayload>. It rejects if the user closes the scanner without capturing, and it rejects immediately when called outside DUST Go (no connector present) — so guard on connector first if the same code path runs in regular browsers.
For multi-scan workflows (e.g. scan-many-then-submit), use the listener API — the scanner stays open across captures:
import { connector } from "@dustid/dust-go-connect";
connector?.add("my-listener", (event) => { switch (event.type) { case "scan": handleScan(event.payload); break; case "hide": // scanner closed case "show": // scanner opened break; default: // Ignore unknown event types — the protocol may grow. break; }});
connector?.showScanner();// later: connector?.hideScanner(); connector?.remove("my-listener");addScanListener is the same thing without the listener bookkeeping — it receives
every scan and returns its own unsubscribe function:
import { addScanListener } from "@dustid/dust-go-connect";
const stop = addScanListener((payload) => handleScan(payload));// later: stop();One scanner session can deliver a series of captures, so submit them one at a time. A DUST payload is a large base64 JPEG; uploading several at once starves the connection and gives the operator no usable progress. Queue the payloads and await each submission before starting the next.
What the scanner can do
Section titled “What the scanner can do”Hosts differ — a phone can adjust zoom and exposure, a USB microscope accessory exposes a different set, and an older build may expose nothing. Ask, rather than inferring from the User-Agent:
const capabilities = connector?.getCapabilities?.();Treat an absent or empty announcement as single captures only, nothing adjustable. Never treat silence as capability.
What’s in the payload
Section titled “What’s in the payload”payload.type |
payload.data |
Notes |
|---|---|---|
DUST |
Base64-encoded JPEG of the DUST capture | Large; resolve it server-side via APID |
QR, BARCODE, DATA_MATRIX |
The decoded symbol contents | |
NFC |
The hex id read from the NFC chip |
payload.metadata (typed ScanMetadata) describes the capture: device identifiers (deviceId, modelName, osName, osVersion, appVersion), lens/camera selection, and optionally optics fields (zoom, focus, exposure, ISO), geolocation (latitude/longitude/accuracy), and capture-source details for external scan accessories (captureSource, usbVendorId, usbProductId, dragonBackend). Forward it opaquely when binding — APID stores it with the Identifier.
Resolve the scan against APID
Section titled “Resolve the scan against APID”The DUST capture is only useful once APID has matched it. Send it as multipart form data with your APID credentials:
async function identifyDustScan(base64Jpeg: string) { // The scan arrives base64-encoded; APID expects binary multipart data. const bytes = Uint8Array.from(atob(base64Jpeg), (c) => c.charCodeAt(0)); const form = new FormData(); form.set("tagType", "DUST"); form.set("data", new Blob([bytes], { type: "image/jpeg" })); // API field names keep legacy "group" naming — these are Team ids. form.set("searchGroupIds", JSON.stringify([TEAM_ID]));
const response = await fetch(`${APID_URL}/api/v1/tags/identify`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Dust-Ctx-Org-Id": ORGANIZATION_ID, }, body: form, });
if (!response.ok) throw new Error(`identify failed: ${response.status}`); return await response.json();}searchGroupIds names the Teams whose Identifiers are searched. The same shape works for /api/v1/tags/bind (add threadId; DUST binds also need a client-generated options.enrollmentSessionId UUID) and /api/v1/tags/verify (add threadId and tags). See Identifiers for the full request/response contracts, and the React Scanner for a copy-ready component that implements all three operations.
Geolocation
Section titled “Geolocation”Standard navigator.geolocation calls work inside DUST Go — the app transparently proxies them through the native OS permission prompt. No connector code needed.
Sign-in flows inside DUST Go
Section titled “Sign-in flows inside DUST Go”If your app uses OAuth/OIDC, be aware that DUST Go hands external identity-provider navigations off to the system browser, and the callback returns to your page via a custom URL scheme.
When you construct an OAuth redirect_uri in the page, always wrap it:
import { connector } from "@dustid/dust-go-connect";
const origin = window.location.origin;const redirectUri = ( connector?.rewriteRedirect(new URL(`${origin}/auth/callback`)) ?? new URL(`${origin}/auth/callback`)).href;Outside DUST Go (and on hosts where no rewrite is needed) this is a no-op. Inside DUST Go it swaps the URL’s protocol for the app’s custom scheme, producing a URI like com.dustidentity.dustgo://your-host/auth/callback (the exact scheme comes from the DUST Go build; dustgo is the fallback), which your identity provider must have registered as an allowed redirect URI.
Test your integration
Section titled “Test your integration”- Install DUST Go from the App Store (see Supported Devices for hardware requirements — DUST capture needs a supported optical accessory).
- Serve your app over HTTPS on a URL the device can reach (a LAN address works for development).
- In DUST Go, add your URL as a custom app link and open it.
- Verify the connector is detected, then run a scan.
A minimal diagnostic page that exercises the whole bridge (detection, scanAsync, event log) is available in the package repository.
Versioning and compatibility
Section titled “Versioning and compatibility”- The connector speaks protocol version 2: at import time it sends an automatic
hellohandshake advertising which events your page supports. You don’t manage this — just keep the package reasonably current. - Event listeners receive every event type; switch on
event.typeand ignore types you don’t recognize (new ones may be added). calibrationResultevents andackCalibrationResults()are internal plumbing for DUST’s first-party calibration workflows — third-party integrations can ignore them.
