Skip to content
instantscan.io docs

Embed SDK

The ~6 KB browser launcher — declarative file-input takeover, the programmatic modal API, every option and error code, and what the SDK deliberately does not do.

The SDK is a launcher, not a scanner. It opens a session, picks the right hand-off for the device (iframe on a phone, QR on a desktop), listens for completion, downloads the result, and hands it to you. That is the whole surface area, and it is deliberately small enough to read in one sitting.

Install

npm install @scanner-cloud/embed-sdk

Or use the script build, which also gives you the declarative mode:

<script src="https://instantscan.io/v1/scanner.js"></script>

Mode A — declarative

Add data-key to the script tag and data-scanner to any file input. There is no application code.

<input type="file" name="id_document" data-scanner />

<script
  src="https://instantscan.io/v1/scanner.js"
  data-key="pk_test_demo"
  data-api="https://api.instantscan.io"
  data-auto-init
></script>
AttributeOnRequiredMeaning
data-key<script>yesYour publishable key.
data-api<script>noAPI base URL. Defaults to the production API.
data-auto-init<script>yes (declarative mode)Opt in to zero-code attach after DOMContentLoaded. Omit in React/Next — call init() yourself.
data-scanner<input type="file">yesMarks the input to take over.

What declarative mode does, in order:

  1. Reads data-key from the script tag.
  2. Waits for DOMContentLoaded so it does not race framework hydration.
  3. Calls init({ autoAttach: true }), which attaches every input[type=file][data-scanner].

Inputs added later are not picked up automatically. Attach them yourself:

const scanner = ScannerSDK.init({
  publicKey: "pk_test_demo",
  apiBase: "https://api.instantscan.io",
});
scanner.attach("#late-input");

attach() is idempotent — calling it twice on the same input is a no-op.

Mode B — React, Next.js, and other SPAs

Do not use data-auto-init in a server-rendered app. The script tag only downloads the launcher; initialization must happen after hydration:

"use client";

import { useEffect, useRef } from "react";

export function ScanField() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    const sdk = window.ScannerSDK;
    if (!sdk || !inputRef.current) return;

    const scanner = sdk.init({
      publicKey: "pk_test_demo",
      apiBase: "https://api.instantscan.io",
    });
    scanner.attach(inputRef.current);

    return () => sdk.destroy();
  }, []);

  return <input ref={inputRef} type="file" name="proof" />;
}

Load the bundle once in your root layout — without data-key or data-auto-init:

<script src="https://instantscan.io/v1/scanner.js" async></script>

init() is idempotent. destroy() removes every scan box and drops the singleton, so route changes and strict-mode double-mounts do not leave duplicate buttons behind.

Mode C — programmatic modal

import { Scanner, ScannerError } from "@scanner-cloud/embed-sdk";

const scanner = Scanner.init({
  publicKey: "pk_test_demo",
  apiBase: "https://api.instantscan.io",
});

const result = await scanner.open({
  metadata: { application_id: "app_1042" },
  allowedOrigins: [window.location.origin],
});

Scanner.init(options)

OptionTypeRequiredMeaning
publicKeystringyesPublishable key (pk_…). Throws ScannerError("config") if missing.
apiBasestringnoAPI base URL, without a trailing slash.

scanner.open(options)

Opens the scanner and resolves once a document has been scanned. On phones and tablets this is a fullscreen iframe; on desktop it shows a QR code and polls until the phone finishes.

OptionTypeMeaning
metadataRecord<string, unknown>Echoed back verbatim in the session, the webhook and analytics.
allowedOriginsstring[]Origins permitted to embed this session and receive its result. Defaults to [window.location.origin]. Every entry must already be on your key’s allowlist.
labelstringText on the scan box in attach() mode.
mode"auto" | "iframe" | "qr"Override device detection. Defaults to auto.

Resolves with:

interface ScanResultFile {
  sessionId: string;
  pageCount: number;
  /** Signed, short-TTL URL of the produced document. */
  url: string;
  /** The document as a File, ready to drop into a form. */
  file: File;
}

scanner.attach(target, options?)

Turns a file input into a scan box. target is an HTMLInputElement or a selector string. Same options as open(). Call from useEffect in SPAs — not at module load time.

scanner.detach(target) / scanner.destroy()

detach() removes one scan box and restores the input. destroy() detaches every input this instance touched. The module-level ScannerSDK.destroy() also drops the singleton from init().

Errors

Rejections are always a ScannerError with a code:

codeWhenWhat to do
cancelledThe user closed the modal or discarded the scan.Nothing. This is not an error path — swallow it.
configMissing publicKey, or session creation was refused (bad key, origin not allowed, no credits).Read the message; it carries the HTTP status and body.
networkThe API or the result download was unreachable.Retry, or fall back to a plain file input.
scan_errorThe hosted scanner reported a failure.Show a retry affordance.
try {
  const { file } = await scanner.open();
} catch (err) {
  if (err instanceof ScannerError && err.code === "cancelled") return;
  showFallbackFileInput();
}

Treating cancelled as an error is the single most common integration mistake. A user changing their mind is normal.

postMessage contract

If you would rather manage the iframe yourself, the SDK is skippable — the hosted scanner posts these to window.parent:

type ScannerMessage =
  | { type: "scanner:ready"; sessionId: string }
  | { type: "scanner:complete"; sessionId: string; pageCount: number; resultUrl: string }
  | { type: "scanner:close"; sessionId: string }
  | { type: "scanner:error"; sessionId: string; message: string };

Two checks are not optional if you do this yourself:

if (event.origin !== "https://scan.instantscan.io") return;
if (event.source !== iframe.contentWindow) return;

Your iframe needs allow="camera", and must not be sandboxed — a sandboxed iframe cannot get camera permission.

What the SDK does not do

By design, and it will not change:

  • No camera access, no OpenCV, no detection, no PDF assembly. Those live on our origin.
  • No result storage. It hands you a File and forgets.
  • No retry of a failed webhook. That is the API’s job.
  • No obfuscation. There is no IP in a launcher, and pretending otherwise would only make it harder for you to debug.

Size budget

The SDK is held under 8 KB gzipped by a build gate. If a change pushes it over, the build fails rather than quietly shipping a heavier launcher. Bundling everything into your page is exactly the outcome this product exists to avoid.