Skip to content
instantscan.io docs

Security model

What each credential can do, how client tokens and file URLs are signed, what we deliberately do not rely on, and the limits we know about.

Everything instantscan.io ships to a browser — the launcher, your publishable key, the session token, the hosted scanner itself — is readable by anyone who opens dev tools. Treating any of it as a secret would be a design error. The boundary is the server, and the blast radius of each browser-side value is deliberately small.

This page is the reference version of that posture: what is enforced, where, and what it does not cover.

Credentials, ranked by what they can do

CredentialLivesCan doCannot do
Publishable key pk_Your HTMLCreate a session, from an allowlisted Origin onlyRead a result, list sessions, change settings, see credits
Client token ?t=A URL, for minutesFetch that session’s config; upload onceAnything after the upload, or for any other session
Secret key sk_Your serverCreate and read sessions and resultsConsole operations (keys, domains, billing)
Console session cookieBrowser, httpOnlyEverything under /v1/dashboard/*Be read by JavaScript

The ordering is the point. The credential most exposed can do least.

Publishable and secret key separation

A publishable key is a session-opening capability and nothing more. It is safe in your HTML because of what it cannot do, not because it is hard to find.

A secret key is trusted. We store only its SHA-256 hash, so the full value crosses the wire exactly once — in the POST /v1/dashboard/keys response — and cannot be recovered afterwards by you or by us.

Revocation is immediate: a revoked key’s next use returns 401 invalid_api_key. Revoked keys stay in the list with revoked_at set, so your history stays intact.

Origin allowlist

Session creation with a publishable key requires the request’s Origin to be on your allowlist, or the API answers 403 origin_not_allowed. A key lifted from your page and used elsewhere is inert.

The same list bounds the session’s allowed_origins — which origins may embed the scanner and receive its scanner:complete message. A session cannot be told to hand its document to an origin you never approved.

Exact origins only. https://acme.example and https://www.acme.example are two entries, and there is no wildcard subdomain support. Ownership is not DNS-verified: the allowlist bounds where a key works, it does not prove you own the domain.

Client tokens

The hosted scanner authenticates with a compact HMAC-signed token carrying:

{
  "sid": "ss_9Qw1p2Kt",
  "tid": "tn_acme",
  "iat": 1735689600000,
  "exp": 1735690500000,
  "jti": "jti_…"
}

Three properties matter:

  • Short-lived. 15 minutes by default. Minutes, not hours.
  • Bound to its session. The jti must match the one stored on the session.
  • Single-use for upload. After a successful upload the session is marked used; a second attempt is 409 token_already_used.

So a token captured from a URL cannot be replayed to inject a second document, and stops working shortly regardless.

Signed file URLs

Result URLs carry ?exp=<epoch ms>&sig=<hmac>. The signature is the authorisation — there is no cookie or bearer token involved, and nothing to guess. Change the id or the expiry and the request is 403 invalid_signature.

Two consequences worth designing around:

  • Anyone holding the URL can fetch the file until it expires. Do not forward it to a browser unless that is what you intend, and do not put it in a log you keep.
  • Download and store the bytes in your webhook handler. Saving the URL and fetching it tomorrow does not work.

Files are dropped after 24 hours by default.

Webhook signing

X-Scanner-Signature: t=1735689600,v1=8f2b1d0c…

v1 is HMAC-SHA256(secret, "<t>.<raw body>"), hex. Verify against raw bytes with a constant-time comparison, and reject timestamps outside a few minutes — a valid signature over a replayed body is still a replay. Full implementation in Webhooks.

The secret is rotatable, and rotation applies to future deliveries only, so you can deploy the new secret on either side of the rotation without dropping events.

Where the document goes

  • On the user’s device: camera access, live edge detection, contour finding, the perspective transform, the document clean-up, text recognition (OCR), PDF assembly. No frame is streamed anywhere, and the OCR model is served from our origin, not a third-party CDN.
  • Over the wire, once: one HTTPS upload of the finished PDF, authenticated by the single-use client token and bound to its session.
  • On our side, briefly: held so you can fetch it, dropped after 24 hours, reachable only through a signed expiring URL.

There is no server-side inference step: detection is geometric, and the one learned model — the OCR that makes the PDF searchable — runs in the user’s browser. Your documents are not training data.

Other server-side controls

  • Rate limiting on session creation, per tenant.
  • Upload caps — 413 above 25 MB by default.
  • Server-side validation of customization, so a hostile accent value cannot become a stylesheet injection. The colour grammar is an allowlist, not “whatever a browser accepts”.
  • Console passwords hashed with scrypt and a per-user salt; hashes never appear in a response.
  • Login does not enumerate accounts — an unknown email and a wrong password give the same 401.
  • Cross-tenant reads are 404, never 403.

Deliberately not relied upon

Not relied uponWhy
Hiding the publishable key or session token in obfuscated JSAnything in the browser can be read. Both values are made safe when extracted instead.
Client-side signature or integrity checksA modified client deletes the check. Decisions that matter are made server-side.
Anti-debugging, DRM theatricsBreaks real browsers, annoys real users, stops nobody trying.
CORS as access controlOur API reflects the caller’s origin and carries no credentials. CORS is a browser convention, not a permission system; enforcement is key validation and the origin allowlist.
Obfuscating the SDK to protect IPA launcher has no IP to protect. Obfuscating it would only make your debugging harder.

IP protection, where it applies at all, applies to the hosted engine on our origin — not to anything on your page.

Known limits

Stated plainly, because a security page listing only strengths is a marketing page:

  • Storage. Results live in the API process with a TTL, not in object storage with server-side encryption and lifecycle rules. Signed URLs and the 24-hour TTL apply either way.
  • Rate limiting is per API instance, not global.
  • Console auth is email and password only. No SSO, no MFA, no roles, no audit log.
  • Upload validation is size and content type. No magic-byte check, no antivirus.
  • Domain ownership is not verified.
  • Certifications. None, and we will not imply otherwise.

If your security review needs answers this page does not give, ask us — we would rather answer a hard questionnaire honestly than have you find a gap after you have shipped.