This walks the whole loop: a session, a scan, and the result in two places. Everything here uses a test key, which costs the same one credit as a live session. Signup grants 250 credits, so you can finish this before buying a pack.
1. Get your keys
Create an account in the console. Signup issues two test keys immediately and grants 250 credits.
- Publishable —
pk_test_…. Safe in your HTML. Can only open sessions, and only from an origin on your allowlist. - Secret —
sk_test_…. Server only. Shown once at creation, stored as a hash afterwards.
If you are running the stack locally, apps/api seeds a demo tenant with fixed keys so every sample
below works without you copying anything:
# Seeded on first boot of apps/api (SEED_DEMO=1, the default).
# Console login: demo@usecollect.com / scanner-demo
PUBLISHABLE_KEY=pk_test_demo
SECRET_KEY=sk_test_demo
2. Allowlist the origin you will embed from
A publishable key only works from an origin you have registered. In the console, open Domains and
add the origin serving your page — http://localhost:3000, for example. A bare hostname or a full
URL both work; we normalise them to an origin.
Without this, session creation from the browser returns 403 origin_not_allowed. That is the
control doing its job, not a bug.
3. Turn a file input into a scanner
The fastest possible integration: one script tag, one attribute. No application code.
<form method="post" action="/applications">
<label for="proof">Proof of address</label>
<input id="proof" type="file" name="proof_of_address" data-scanner />
<button type="submit">Submit</button>
</form>
<script
src="https://instantscan.io/v1/scanner.js"
data-key="pk_test_demo"
data-api="https://api.instantscan.io"
data-auto-init
></script>
The launcher hides the real input, renders a “Scan a document” box in its place, and when the scan
finishes it writes the PDF into that input as a real File and fires a change event. Your form
submits exactly as it did before — the server receives a multipart file upload and cannot tell the
difference.
Open the page on a phone (or a laptop with a webcam), tap the box, and scan something.
Nothing happened when I tapped the box. Check the browser console. The two common causes are an origin that is not on your allowlist (
403) and adata-apipointing somewhere that is not running. Both surface as aSession create failederror from the SDK.
4. Or call it yourself
If your input is controlled by a framework, or the scan is not attached to a form at all, use the
programmatic API. open() resolves with a File.
import { Scanner } from "@scanner-cloud/embed-sdk";
const scanner = Scanner.init({
publicKey: "pk_test_demo",
apiBase: "https://api.instantscan.io",
});
document.querySelector("#scan").addEventListener("click", async () => {
try {
const { file, pageCount, url } = await scanner.open({
metadata: { application_id: "app_1042" },
});
console.log(`${pageCount} page(s), ${file.size} bytes`);
// `url` is a signed, short-lived download URL for the same PDF.
} catch (err) {
if (err.code !== "cancelled") throw err;
}
});
metadata is opaque to us. Whatever you put there comes back verbatim in the session, the webhook
and your analytics — it is how you tie a scan back to a row in your own database.
5. Receive the result on your server
The browser is a nice place to show a result and a bad place to depend on one: the user can close the tab, and in a QR flow there is no browser on your side at all. So the canonical delivery is a webhook.
In the console, open Webhooks, set your endpoint, and copy the signing secret. Then verify every delivery:
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.SCANNER_WEBHOOK_SECRET!; // whsec_…
app.post("/webhooks/scanner", express.raw({ type: "*/*" }), (req, res) => {
// Verify against the raw bytes. A re-serialised body will not match.
if (!verify(req.body.toString("utf8"), req.get("X-Scanner-Signature") ?? "", SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "scan.completed") {
// event.result.url is signed and short-lived — fetch it now, store the bytes.
void download(event.result.url, event.metadata.application_id);
}
// Answer 2xx quickly; we retry anything else with backoff.
res.sendStatus(204);
});
The verify helper is eight lines and is written out in full in Webhooks.
Use Send test event in the console to get a test.ping delivery without scanning anything, then
read the delivery log to see the exact request we sent and the response you returned.
6. Check it in the console
The Overview screen shows sessions created, completion rate, pages and credits over the last 30 days. Your test scan appears there immediately, alongside a status breakdown that tells you whether users are finishing or abandoning.
The scan appears in analytics immediately, tagged test, and as a -1 row in the credit ledger.
Where to go next
- Embed SDK — every option on the launcher.
- Customization — language, colours, watermark, success screen, capture limits.
- QR hand-off — desktop users with no camera.
- Going live — the checklist before you switch to live keys.