Switching from test to live is a key swap. Everything below is about the things that a key swap does not fix.
1. Create live keys
In the console, Keys → Create key. You need:
- One live publishable key, if the browser opens sessions.
- One live secret key, if your server does.
The secret value is shown exactly once. We store only its hash, so there is no “show me again” — put it straight into your secret manager.
Keep the test keys. Staging should keep using them so production analytics stay clean, and so you can revoke staging credentials without touching live. Both modes cost a credit per scanned document.
2. Allowlist your production origins
Add every origin that will embed the scanner: your production domain, any www variant, and your
staging domain if it uses live keys.
Scheme, host and port all matter. https://acme.example and https://www.acme.example are two
entries. There are no wildcard subdomains, so a per-customer subdomain scheme needs each origin added
explicitly.
3. Move secrets out of your code
Three values belong in your secret manager, not your repository:
| Value | Used by |
|---|---|
sk_live_… | Your server, creating sessions and reading results. |
whsec_… | Your webhook handler, verifying signatures. |
| — | The publishable key is not a secret. It belongs in your HTML. |
4. Harden the webhook handler
The single most common cause of a bad first week in production. Before you switch:
- Verify the signature against the raw bytes. Not a parsed and re-serialised body.
- Reject stale timestamps. A few minutes of tolerance.
- Be idempotent on
session_id. Retries and manual replays both mean you can see an event twice. - Answer
2xxfast, work afterwards. Queue the download; do not do it inside the handler. - Download
result.urlimmediately and store the bytes. The URL is short-lived by design. - Accept both secrets during a rotation. Then rotate, then drop the old one.
Send a test event to production once it is deployed, and confirm it appears in the delivery log with a
2xx.
5. Build the fallback path
Scanning can fail for reasons entirely outside our control: a denied camera permission, an ancient in-app browser, a corporate device with the camera disabled by policy.
Keep a plain file input as the fallback and offer it whenever the SDK rejects with anything other than
cancelled:
try {
const { file } = await scanner.open();
attach(file);
} catch (err) {
if (err instanceof ScannerError && err.code === "cancelled") return;
reportToSentry(err);
showPlainFileInput(); // degrade, do not dead-end
}
Also handle 402 insufficient_credits on the server as a degraded path rather than an error page.
Users should never see a billing problem.
6. Set the scanner defaults you actually want
Reasonable production defaults, in the console under Scanner:
locale— set it, or override per session when you know the user’s language. English by default is a choice, not an absence of one.maxPages— cap it at what the document actually is.1for an ID removes all ambiguity.completion.message— tell the user what happens next. “Document received” plus your own next step beats a generic tick.grayscale— turn it on unless colour carries information. It roughly halves file size.theme.accentandbrandName— so the scanner does not look like a third-party pop-up, which is what users abandon.
7. Pre-flight checklist
| ✔ | Item |
|---|---|
| ☐ | Live keys created; secret stored in a secret manager, not the repo. |
| ☐ | Production origins on the allowlist, including www. |
| ☐ | Webhook endpoint is https, verified, idempotent, and answers 2xx quickly. |
| ☐ | Test event delivered to production and visible in the delivery log. |
| ☐ | result.url downloaded and persisted by your handler, not stored as a URL. |
| ☐ | 402 handled as a degraded path with an alert to your team. |
| ☐ | cancelled handled as a normal outcome, not an error. |
| ☐ | Plain file input fallback in place. |
| ☐ | Scanner defaults set: locale, maxPages, completion message. |
| ☐ | A credit pack bought — the 250 signup credits are for building, not for launching. |
| ☐ | Someone owns the “credits are low” alert. |
8. Watch these in the first week
From the console Overview:
- Completion rate. Below ~70% is almost always a placement problem: you are asking for the scan before the user has the document to hand.
expiredcount. Users opening the scanner and walking away. In QR flows, usually a code shown too early.failedcount. Real errors. Cross-reference with the delivery log and your own error tracking.spent_30d. Your actual burn rate, which is the only reliable input to when you need the next pack.
Known limits
Worth knowing before you commit, rather than discovering later:
- Result files are held for 24 hours and then dropped. Download them.
- A session holds one document. Multiple documents means multiple sessions, and multiple credits.
- Sessions expire 15 minutes after creation.
- Uploads are capped at 25 MB.
- Rate limiting is per tenant and currently per API instance, not global.
- Only PDF output exists.
- No right-to-left locales yet.
- The console has no SSO, no roles and no audit log — one account per tenant.
See Security model for the full posture, including what we deliberately do not rely on.