The scanner runs on our origin, so you cannot style it with your own CSS. Instead there is a fixed set
of options, each of which maps to a control the engine genuinely has. Every option is validated
server-side before it is stored, so a bad value is a 400 at save time rather than a broken camera UI
in production.
Profiles and per-session overrides
Create named scanner profiles in the console (Scanner) for experiences such as “French”, “English”, or “ID capture”. Exactly one profile is the default.
Pass scanner_id when creating a session to select a profile. If it is omitted, the default profile
is used. You may also pass scanner; it is deep-merged over the selected profile, so only the keys
you send move and the saved profile is not touched.
curl -X POST https://api.instantscan.io/v1/sessions \
-H "Authorization: Bearer sk_test_demo" \
-H "Content-Type: application/json" \
-d '{
"metadata": { "application_id": "app_1042" },
"scanner_id": "scp_french",
"scanner": {
"options": { "maxPages": 2 }
}
}'
That session starts from the French profile with a two-page cap. Its locale, accent, brand name, success screen and every other capture flag still come from the selected profile.
Per-session overrides are the right tool when you already know something the account cannot: the user’s language, or that this document is a single-page ID rather than a contract.
The full object
{
"locale": "en",
"prompt": "Place your passport flat inside the frame",
"theme": {
"accent": "#1a90cc",
"cornerRadius": 10,
"brandName": "Acme Lending",
"logoUrl": "https://acme.example/logo.svg"
},
"completion": {
"message": "Document received — you can close this window.",
"imageUrl": "https://acme.example/done.png"
},
"options": {
"maxPages": 20,
"output": "pdf",
"grayscale": false,
"showFlash": true,
"showZoom": true,
"showA4Hint": true
},
"watermark": {
"enabled": true,
"text": "Acme Lending · Secure scan",
"color": "#334155",
"opacity": 0.18,
"size": "md"
}
}
Language
{ "locale": "fr" }
Supported: en, fr, es, de, pt, nl. Anything else is rejected with
fields: { "locale": "must be one of en, fr, es, de, pt, nl" }.
Every visible string in the scanner is translated, including error messages and the page-limit warning. There is no partial state where the buttons are French and the errors are English.
Right-to-left layout is not supported yet, so Arabic and Hebrew are absent rather than half-implemented.
Capture prompt
{ "prompt": "Place your passport flat inside the frame" }
The single line shown over the camera while the user lines up a page. Leave it unset to use the localised default (“Put the document at the center of the frame” and its translations); set it to speak to your specific document — “Photograph the signature page” reads very differently from a generic hint.
| Field | Type | Rules |
|---|---|---|
prompt | string | 80 characters or fewer. Empty falls back to the localised default. |
Unlike the completion message, the prompt has a translated default, so you only need to set it when you want to override that default — not merely to get a sensible string in each locale.
Theme
| Field | Type | Rules |
|---|---|---|
accent | string | A CSS colour: hex (#1a90cc), rgb()/rgba(), hsl()/hsla(), or one of a small set of named colours. |
cornerRadius | number | 0–32, in px. Rounded to a whole number. |
brandName | string | 40 characters or fewer. Shown in the scanner chrome and the document title. |
logoUrl | string | https: URL or a same-origin path. |
The accent applies to the shutter, the detected-edge outline and the primary buttons. Pick something with contrast against a dark camera view — a pale yellow accent is legal and unreadable.
accent is deliberately validated against a narrow allowlist rather than “anything a browser
accepts”, because the value is interpolated into a stylesheet. red; } body { display: none is not a
colour and will be refused.
Completion screen
Shown after the document is sent — the last thing the user sees, and the only chance to tell them what happens next.
| Field | Type | Rules |
|---|---|---|
message | string | 120 characters or fewer. |
imageUrl | string | https: URL or a same-origin path. |
{
"completion": {
"message": "Thanks! We are reviewing your document and will email you within an hour.",
"imageUrl": "https://acme.example/check.svg"
}
}
- The message is not translated — it is your copy, in whichever language you wrote it. If you
serve several locales, use one profile per locale or set it per session alongside
locale. - The scanner remains on this screen until the user presses Done. Browsers do not reliably allow a page opened from a QR code to close itself.
Capture options
| Field | Type | Rules | Effect |
|---|---|---|---|
maxPages | number | Whole number, 1–50 | The shutter refuses further captures at the limit and explains why, in the configured locale. |
output | string | "pdf" | Only PDF exists today. Any other value is a 400. |
grayscale | boolean | Converts pages to grayscale. Typically halves the PDF size. | |
showFlash | boolean | Shows the torch control, where the device supports it. | |
showZoom | boolean | Shows the zoom control, where the device supports it. | |
showA4Hint | boolean | Shows the A4 alignment guides. |
Setting maxPages: 1 is the single highest-leverage option for ID capture: it removes the “am I done?”
ambiguity entirely, because the scanner finishes as soon as the page is captured.
When a control is switched off it is not rendered at all. The engine sets control visibility imperatively, so “off” means removed rather than hidden-but-clickable.
What every scan gets
These are not options. They are how the scanner works for every tenant, on the user’s device.
- A steady edge lock. The outline follows the page and holds through a brief missed frame instead of blinking. The ring around the shutter fills while a centred page is held still, and the page is captured when it closes, in about a second and a half.
- A prompt when the phone shadows the page. If the phone’s own shadow darkens part of the page,
the scanner says so and offers Turn on light where the device has a torch and
showFlashis notfalse. It never switches the torch on by itself. Without a torch, it asks the user to lift the phone. - A page that reads like a scan. Each confirmed page has its lighting evened out, the paper set to
white, the ink darkened and the type sharpened. Colour is kept, so stamps, logos and signatures
survive. The gallery shows exactly the page that will be sent.
grayscaleand the watermark are applied after this step. - A searchable PDF. Text is recognised on the device and laid over each page as an invisible
layer, so the PDF can be searched and its text copied. Recognition uses the session’s
localeplus English. It is best-effort: on Send the scanner waits up to ten seconds for pages still being read, then sends them as images. The result is still one PDF, so nothing changes in your integration.
Document watermark
A short line of text, repeated in a light diagonal pattern over every page. Your user sees it on the camera view, on the crop screen and on every page in the gallery, so they can tell before sending that the document is marked as yours. The same pattern is stamped into the delivered PDF.
{
"watermark": {
"enabled": true,
"text": "Acme Lending · Secure scan",
"color": "#334155",
"opacity": 0.18,
"size": "md"
}
}
| Field | Type | Rules |
|---|---|---|
enabled | boolean | Default false. Off means no mark on screen and none in the PDF. |
text | string | 40 characters or fewer. Required when enabled is true. |
color | string | A CSS colour, with the same rules as theme.accent. Default #334155. |
opacity | number | 0.08–0.35. Default 0.18. |
size | string | "sm", "md" or "lg". Default "md". Scales with the page, so it looks the same on every device. |
- The text is not translated — it is your copy. Use one profile per language, or set it per
session alongside
locale. - The opacity cap is intentional: above it, a mark starts hiding the content your team needs to read.
- The mark is applied in the scanner, on the user’s device, when the document is sent. It is a visible label on the document, not a tamper-proof signature. Treat the PDF your webhook receives as the source of truth.
- Grayscale is applied first, so a coloured mark stays coloured on a grayscale scan.
Reading the merged result
The scanner fetches its own configuration at boot, and you can fetch the same thing:
curl "https://api.instantscan.io/v1/scanner/config?t=<client_token>"
{
"session_id": "ss_9Qw1p2Kt",
"upload": { "url": "https://blob.vercel-storage.com/scans/ss_9Qw1p2Kt.pdf?token=…", "...": "…" },
"complete_url": "https://api.instantscan.io/v1/scanner/complete",
"upload_url": "https://api.instantscan.io/v1/scanner/upload",
"scanner": { "locale": "fr", "theme": { "accent": "#1a90cc", "cornerRadius": 10 }, "...": "…" }
}
The scanner object is the exact one the scanner renders from, after merging. This is the fastest way
to confirm an override actually landed. The upload fields are covered in the
API reference.
Validation errors
Failures come back as a field map, all problems at once, so a form can display every error in one round trip:
{
"error": "invalid_request",
"fields": {
"locale": "must be one of en, fr, es, de, pt, nl",
"theme.cornerRadius": "must be between 0 and 32",
"options.maxPages": "must be a whole number between 1 and 50"
}
}
Nothing is persisted when validation fails — a rejected save leaves your previous settings exactly as they were.
On session creation the same failure is reported as invalid_scanner_config with the same fields
map, and no session is created, so you are not charged for a session with a broken configuration.
Deliberate non-goals
- Custom CSS or JavaScript. A camera UI has a very small margin for error; a stylesheet that breaks the shutter breaks the scan.
- Hosting your brand assets. Point
logoUrlandcompletion.imageUrlat your own CDN overhttps. - Per-key defaults. Select a profile with
scanner_id; keys do not carry scanner settings. - Auto-capture settings. The countdown is not configurable: it only fires on a centred page held still, the user can always tap the shutter instead, and every capture goes through the crop screen before it is kept.