QR Capture Guide
Generate a QR code on your desktop, let users scan it with their phone to capture documents, and receive results via webhooks — all without installing the Lens SDK on the desktop side.
Authorization
Every request to the QR generation API requires two headers:
| Header | Value |
|---|---|
CLIENT-ID | Your Client ID |
AUTHORIZATION | apikey {username}:{api_key} |
Generate a QR Code
Send a POST request to your Lens for Browser backend:
curl -X POST https://lens.veryfi.com/api/generate_qr \
-H "Content-Type: application/json" \
-H "CLIENT-ID: your_client_id" \
-H "AUTHORIZATION: apikey your_username:your_api_key" \
-d '{
"client_id": "your_client_id",
"external_id": "ORDER-12345",
"mode": "document",
"expires_in": 3600,
"is_async": true
}'
Response
{
"session_id": "c01af7da-ce5f-43a2-abc4-eb21e2e82f61",
"external_id": "ORDER-12345",
"url": "https://lens-pwa.veryfi.com/capture/scan?client_id=...&session=...",
"qr_code_base64": "data:image/png;base64,iVBORw0KGgo...",
"expires_in": 3600
}
Display the QR by setting qr_code_base64 as the src of an <img> tag:
const img = document.createElement("img");
img.src = data.qr_code_base64;
img.width = 280;
img.height = 280;
document.body.appendChild(img);
Request Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
client_id | string | Yes | — | Your Veryfi Client ID |
external_id | string | No | auto-generated | Your own correlation ID — used to match webhook results back to this session |
mode | string | No | "document" | Capture mode: "document" (receipts/invoices) or "anydocs" |
blueprint | string | No | — | Blueprint slug for anydocs mode (e.g. "us_health_insurance_card") |
expires_in | number | No | 3600 | QR code validity in seconds (60–86400) |
base_url | string | No | — | Base URL of the app that hosts the capture page (used to build the QR link) |
is_async | bool | No | false | When true, document processing happens asynchronously and results are delivered via webhook |
Capture Modes
document— Optimized for receipts and invoices. Single-page capture with automatic document detection.anydocs— Flexible multi-page capture. Supports blueprints for structured data extraction (insurance cards, IDs, etc.). Useblueprintto pre-select a template.
Receiving Webhooks
After the user scans the QR and submits a document, Veryfi processes it and sends a webhook to your configured URL.
Webhook Payload
{
"event": "document.created",
"data": {
"id": 8724086,
"created": "2026-03-26T18:30:00Z"
}
}
The webhook contains the document ID but not the full document data. Fetch the complete document using the ID:
curl -X GET https://api.veryfi.com/api/v8/partner/documents/8724086 \
-H "CLIENT-ID: your_client_id" \
-H "AUTHORIZATION: apikey your_username:your_api_key"
For anydocs, use the /any-documents/ endpoint instead:
curl -X GET https://api.veryfi.com/api/v8/partner/any-documents/8724086 \
-H "CLIENT-ID: your_client_id" \
-H "AUTHORIZATION: apikey your_username:your_api_key"
Webhook Event Types
| Event Prefix | Document Type | Fetch Endpoint |
|---|---|---|
document.* | Receipts / Invoices | /api/v8/partner/documents/{id} |
anydocument.* | Any Documents | /api/v8/partner/any-documents/{id} |
check.* | Checks | /api/v8/partner/checks/{id} |
Setting Up Webhooks Locally
For local development, use ngrok to expose your webhook endpoint:
ngrok http 3000
Then configure the webhook URL in your Veryfi dashboard under Settings > Keys > Webhook URL:
https://a1b2c3d4.ngrok-free.app/api/webhook
Full Integration Example
A minimal server-side handler that generates a QR code, receives the webhook, fetches the full document, and pushes the result to the frontend via Server-Sent Events:
import express from "express";
import { EventEmitter } from "events";
const CLIENT_ID = process.env.CLIENT_ID;
const USERNAME = process.env.USERNAME;
const API_KEY = process.env.API_KEY;
const LFB_BASE_URL = process.env.LFB_BASE_URL;
const VERYFI_API_BASE = "https://api.veryfi.com/api/v8/partner";
const app = express();
app.use(express.json());
const emitter = new EventEmitter();
const AUTH_HEADERS = {
"CLIENT-ID": CLIENT_ID,
AUTHORIZATION: `apikey ${USERNAME}:${API_KEY}`,
};
const EVENT_ENDPOINTS = {
document: "documents",
anydocument: "any-documents",
check: "checks",
};
// 1. Generate QR
app.post("/api/generate-qr", async (req, res) => {
const { external_id, mode, blueprint, expires_in } = req.body;
const response = await fetch(`${LFB_BASE_URL}/api/generate_qr`, {
method: "POST",
headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
body: JSON.stringify({
client_id: CLIENT_ID,
external_id: external_id || `session-${Date.now()}`,
mode: mode || "document",
...(blueprint ? { blueprint } : {}),
expires_in: expires_in || 3600,
is_async: true,
}),
});
const data = await response.json();
res.json(data);
});
// 2. Receive webhook → fetch full document → push to frontend
app.post("/api/webhook", async (req, res) => {
const payload = req.body;
const event = payload.event || "";
const documentId = (payload.data || payload).id;
if (!documentId) return res.json({ status: "ok" });
const prefix = event.split(".")[0] || "any-documents";
const endpoint = EVENT_ENDPOINTS[prefix] || "any-documents";
const docResponse = await fetch(
`${VERYFI_API_BASE}/${endpoint}/${documentId}`,
{ headers: AUTH_HEADERS }
);
const document = await docResponse.json();
const externalId = document.meta?.external_id || document.external_id;
emitter.emit("webhook", { external_id: externalId, document });
res.json({ status: "ok" });
});
// 3. SSE stream filtered by external_id
app.get("/api/webhook-events", (req, res) => {
const externalId = req.query.external_id;
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
});
res.write(`data: ${JSON.stringify({ type: "connected" })}\n\n`);
const onEvent = (event) => {
if (!externalId || event.external_id === externalId) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
};
emitter.on("webhook", onEvent);
req.on("close", () => emitter.off("webhook", onEvent));
});
app.listen(3000);
On the frontend, subscribe to the SSE stream after generating the QR:
const eventSource = new EventSource(
`/api/webhook-events?external_id=${encodeURIComponent(externalId)}`
);
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === "connected") return;
// Document received — show result, close stream
console.log("Document:", data.document);
eventSource.close();
};
Demo Project
A complete working example with a Vite frontend and Express backend is available in the lfb-qr-demo repository.
Next Steps
- Configuration Options - Explore all available settings
- UI Customization - Style the capture scanner with CSS
- Event Handling - Learn about all available events