Skip to content
instantscan.io docs

Quickstart

Go from nothing to a scanned PDF arriving in your form, and then to a verified webhook on your server. About ten minutes, on the 250 signup credits.

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 a data-api pointing somewhere that is not running. Both surface as a Session create failed error 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.