Web SDK
A camera scanner and API client for the browser that never uploads a frame.
On this page
@receviz/web is ReceViz for the browser: a camera scanner that finds the document, waits for a sharp, still, well-lit moment and takes one picture, and a client for the extraction API.
Frames stay on the device#
The camera's frames are analysed in the page, and no frame is ever uploaded. The only upload is the one picture your code sends to POST /v1/extractions. Frames are reduced to the same two small grids the phone apps use (96 cells of brightness across, and a quarter-size grid of edge energy measured at 1280 px), so the sharpness and lighting thresholds mean the same thing on every platform.
The detection and capture decisions are the ones ReceiptVis Live, the scanner in AccountHouse's own phone apps, makes on Android and iOS: the SDK carries a copy of the same engine.
Install#
The package is not on npm yet. It is built as:
receviz.mjs, an ES module, with type declarations;receviz.min.js, a single script that setswindow.ReceViz.
Chrome and Edge 90 or later, Safari 15 or later and Firefox 90 or later are supported. The camera needs a secure context: https, or localhost while you develop.
Credentials#
A browser must never hold a secret key, and the client refuses one in a page. Give it exactly one of these:
| Option | What it is | When |
|---|---|---|
tokenProvider | A function that asks your server for a client token. Your server calls POST /v1/client-tokens with its secret key. | Recommended |
publishableKey | rv_pk_…, exchanged for a ten-minute client token. Works only from the origins listed on the key. | Pages with no server of their own |
token | A client token you already have (rv_ct_…). | Short sessions you manage yourself |
A client token never carries a capability that manages the account, and can be limited to named document types. With tokenProvider or publishableKey, the client fetches a new token shortly before the old one expires, and once more if ReceViz answers token_expired. Your server's side of tokenProvider:
import express from "express";
const RECEVIZ_API = "https://accounthouse-backend-793493499887.me-central1.run.app/api/receviz/v1";
const app = express();
// requireSignedIn is your own middleware: only people who may scan get a token.
app.post("/api/receviz-token", requireSignedIn, async (req, res) => {
const response = await fetch(`${RECEVIZ_API}/client-tokens`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RECEVIZ_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ttl_seconds: 600, document_types: ["payment_receipt"] }),
});
const body = await response.json();
if (!response.ok) return res.status(502).json({ error: body.error.code });
res.json({ token: body.token, expires_at: body.expires_at });
});baseUrl points the client at another address; by default it is https://accounthouse-backend-793493499887.me-central1.run.app/api/receviz/v1. The client sends X-ReceViz-SDK: web/0.1.0 with every request.
Scan and extract#
import { Client, openScanner } from "@receviz/web";
const client = new Client({
// Your server mints the token (POST /v1/client-tokens) after its own sign-in.
tokenProvider: async () => {
const response = await fetch("/api/receviz-token", { method: "POST" });
return (await response.json()).token;
},
});
const { document, extraction } = await openScanner({
client,
documentType: "payment_receipt",
mode: "standard",
noun: "receipt",
});
console.log(extraction.data); // { amount: 150, currency: "AED", transaction_date: "2026-09-26", … }
console.log(extraction.review.required); // true when a person should check itThe scanner opens over the page (or inside container), guides the person, lets them keep or retake the picture, uploads it and resolves with the extraction. Closing it rejects with a ReceVizError of type cancelled. When the camera is refused or missing, or the page is not on https, it offers a file instead. Without a client, it resolves with extraction: null and you upload document.blob yourself.
clientClientUpload the picture and return the extraction.
documentType, modestringAs in
POST /v1/extractions.extractobjectAnything else for the extraction:
options,metadata,idempotencyKey,async…noun, titlestringWhat to call the document in the guidance, and the dialog's heading. Leave them out: the scanner works the noun out from
documentTypeat once (“Scan an invoice”), then uses the document type's owncapture.nounwhen the client can read document types.autoCapturebooleanTake the picture at the best moment on its own.
cleanUpbooleanWhite paper and dark print, as the phone apps do.
allowFileUploadbooleanOffer “Choose a file” beside the shutter. Default true.
containerHTMLElementRender inside this element instead of over the page.
signalAbortSignalClose the scanner from your code.
onGuidancefunctionReceives each guidance text as it changes.
With a script tag:
<script src="receviz.min.js"></script>
<script>
const client = new ReceViz.Client({ publishableKey: "rv_pk_live_…" });
document.querySelector("#scan").addEventListener("click", async () => {
const { extraction } = await ReceViz.openScanner({ client, documentType: "invoice", noun: "invoice" });
});
</script>Theming#
The scanner's markup lives in a shadow root, so your page's styles cannot reach it and its styles cannot leak. Set these custom properties on the page or on container:
:root {
--rv-font: "Your Font", sans-serif;
--rv-accent: #2B4EEA; /* primary buttons */
--rv-highlight: #FFD84A; /* the outline when the picture is ready */
--rv-stage: #16213E; /* the camera surround */
}Your own camera screen#
DocumentCamera is the scanner without a user interface: you draw the screen, it tells you what it sees.
import { DocumentCamera } from "@receviz/web";
const camera = new DocumentCamera({
video: document.querySelector("video"),
noun: "invoice",
onUpdate: ({ snapshot, guidance }) => {
hint.textContent = guidance; // "Move closer", "Hold steady", …
drawOutline(snapshot.outline, snapshot.quality.capturable);
},
onCapture: async (doc) => {
const extraction = await client.extract(doc.blob, { documentType: "invoice" });
},
});
await camera.start();
// camera.capture() is the shutter; camera.retake() resumes; camera.stop() releases the camera.Its options include autoCapture and cleanUp (both on by default), fps (frames analysed per second, default 8), facingMode, and onError. DocumentCamera.isSupported() says whether the browser can run the camera at all. A captured document is a JPEG blob, cropped to the document and straightened when an outline was found, with its size, the outline used, the frame's quality, and whether the shutter or auto-capture took it.
Files people choose#
import { normalizeImage } from "@receviz/web";
const { blob } = await normalizeImage(file); // upright, long side ≤ 2400 px (3264 for long receipts)
const extraction = await client.extract(blob, { documentType: "invoice" });normalizeImage turns a chosen photo upright and scales it so its long side is at most 2,400 pixels, or 3,264 for a long receipt. PDFs pass through unchanged, and so do images the browser cannot decode (HEIC outside Safari), which ReceViz reads itself. See formats.
The client#
extract(document, options)Send a
BloborFile(ornullwithclientOcr). Options:documentType,schemaVersion,mode,async,options,metadata,clientOcr,idempotencyKey,signal. A queued extraction is polled until it finishes unlesswaitForResult: false.getExtraction(id)One extraction, as it stands.
waitForExtraction(id, { timeoutMs })Poll until it succeeds or fails: every second at first, backing off to eight. Default timeout ten minutes, then a
processing_errorwith codewait_timeout; the extraction carries on, so fetch it later.documentTypes()The document types the credential may use.
me()The credential's application, mode and capabilities.
sendQualityFeedback(report)How an on-device reading compared with ReceViz's: counts and verdicts only, never values.
Errors#
Every failure is a ReceVizError with type, code and message, and for API errors status, requestId and retryAfter. Quote the requestId when you ask for help.
| type | Typical cause |
|---|---|
authentication_error | Missing, expired or revoked credential. An expired client token is renewed once on its own. |
permission_error | The credential lacks the capability, or the origin is not allowed. |
rate_limit_error | Too many requests. Wait retryAfter seconds. |
invalid_request_error | Unsupported file, file too large, unknown document type. |
processing_error | The document could not be read in time, or waitForExtraction timed out. |
camera_error | Camera refused (permission_denied), missing (no_camera), busy (camera_busy) or not allowed on http (camera_unsupported). |
network_error | ReceViz could not be reached. |
configuration_error | The client was set up wrongly, for example with a secret key in a page. |
cancelled | The person closed the scanner, or your signal aborted. |
The API's own codes
For API errors, code is the API's code; each is explained in Errors.