Salesforce QR Capture Guide
A Lightning Web Component that lets an authenticated Salesforce user generate a Veryfi QR code, capture a document on their phone, and see the OCR'd document JSON on the page when processing completes.
Copy the source below into a Salesforce DX project — no separate bundle download required. For QR API details (headers, modes, webhooks), see the QR Capture Guide.
Prerequisites:
- Veryfi CLIENT ID, USERNAME, and API KEY from Keys
- Lens For Browser enabled for your account (confirm with Customer Support)
- Salesforce CLI (
sf --version) authenticated to your target org (sf org login web)
Project layout
Create these paths under your DX project root:
force-app/main/default/
├── classes/
│ ├── VeryfiLensConfig.cls
│ ├── VeryfiLensConfig.cls-meta.xml
│ ├── VeryfiQRService.cls
│ ├── VeryfiQRService.cls-meta.xml
│ ├── VeryfiWebhookEndpoint.cls
│ └── VeryfiWebhookEndpoint.cls-meta.xml
├── lwc/veryfiQrCapture/
│ ├── veryfiQrCapture.js
│ ├── veryfiQrCapture.html
│ ├── veryfiQrCapture.css
│ └── veryfiQrCapture.js-meta.xml
├── objects/Veryfi_Capture__c/
│ ├── Veryfi_Capture__c.object-meta.xml
│ └── fields/
│ ├── External_Id__c.field-meta.xml
│ ├── Status__c.field-meta.xml
│ ├── Document_Id__c.field-meta.xml
│ ├── Document_Type__c.field-meta.xml
│ └── Document_Json__c.field-meta.xml
├── permissionsets/
│ ├── Veryfi_QR_Capture_User.permissionset-meta.xml
│ └── Veryfi_Webhook_Service.permissionset-meta.xml
└── remoteSiteSettings/
├── Veryfi_Lens.remoteSite-meta.xml
└── Veryfi_API.remoteSite-meta.xml
Source code
- Apex
- LWC
- Custom object
- Permissions & remote sites
classes/VeryfiLensConfig.cls
Replace the three credential placeholders with your Veryfi keys.
/**
* Central credentials for the Veryfi Lens integration.
* Replace the three placeholder values below with your Veryfi credentials
* (from the Veryfi dashboard → Settings → Keys).
*
* For production, consider moving these to a Named Credential, Custom Metadata
* Type, or protected Custom Setting so they aren't in source control.
*/
public class VeryfiLensConfig {
public static final String CLIENT_ID = 'YOUR_VERYFI_CLIENT_ID';
public static final String API_KEY = 'YOUR_VERYFI_API_KEY';
public static final String USERNAME = 'YOUR_VERYFI_USERNAME';
public static final String LENS_BASE_URL = 'https://lens.veryfi.com';
public static final String API_BASE_URL = 'https://api.veryfi.com';
}
classes/VeryfiLensConfig.cls-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<status>Active</status>
</ApexClass>
classes/VeryfiQRService.cls
/**
* Server-side support for the Veryfi QR Capture LWC.
*
* generateQR() — creates a Veryfi QR session, returns the QR + deep link + external_id.
* fetchCapture() — looks up the Veryfi_Capture__c record the webhook inserted.
*
* Sharing: declared `with sharing` so fetchCapture respects record ownership. Each user
* only sees captures they own (records are owned by the user who generated the QR,
* via OwnerId parsed from the external_id by VeryfiWebhookEndpoint).
*/
public with sharing class VeryfiQRService {
public class QrResult {
@AuraEnabled public String qrUrl;
@AuraEnabled public String linkUrl;
@AuraEnabled public String sessionId;
@AuraEnabled public String externalId;
}
@AuraEnabled
public static QrResult generateQR() {
try {
String externalId = buildExternalId();
HttpRequest req = new HttpRequest();
req.setEndpoint(VeryfiLensConfig.LENS_BASE_URL + '/api/generate_qr');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('CLIENT-ID', VeryfiLensConfig.CLIENT_ID);
req.setHeader('AUTHORIZATION', 'apikey ' + VeryfiLensConfig.USERNAME + ':' + VeryfiLensConfig.API_KEY);
Map<String, Object> body = new Map<String, Object>{
'client_id' => VeryfiLensConfig.CLIENT_ID,
'external_id' => externalId,
'mode' => 'document',
'expires_in' => 3600,
'is_async' => true
};
req.setBody(JSON.serialize(body));
req.setTimeout(60000);
HttpResponse res = new Http().send(req);
if (res.getStatusCode() < 200 || res.getStatusCode() >= 300) {
throwAura('Veryfi API error ' + res.getStatusCode() + ': ' + res.getBody());
}
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
QrResult result = new QrResult();
result.qrUrl = (String) data.get('qr_code_base64');
result.linkUrl = (String) data.get('url');
result.sessionId = (String) data.get('session_id');
result.externalId = (String) data.get('external_id');
return result;
} catch (AuraHandledException e) {
throw e;
} catch (Exception e) {
throwAura(e.getTypeName() + ': ' + e.getMessage());
return null;
}
}
@AuraEnabled
public static Veryfi_Capture__c fetchCapture(String externalId) {
List<Veryfi_Capture__c> records = [
SELECT Id, External_Id__c, Status__c, Document_Id__c, Document_Type__c, Document_Json__c
FROM Veryfi_Capture__c
WHERE External_Id__c = :externalId
LIMIT 1
];
return records.isEmpty() ? null : records[0];
}
/**
* external_id format: sf-<userId>-<timestampMs>-<16hex>
* - userId → so the webhook can attribute the record to the right SF user
* - timestampMs → ordering / dedupe within a session
* - 16 random hex → entropy so an attacker can't guess another user's id
*/
private static String buildExternalId() {
String randomHex = EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, 16);
return 'sf-' + UserInfo.getUserId()
+ '-' + String.valueOf(Datetime.now().getTime())
+ '-' + randomHex;
}
private static void throwAura(String msg) {
AuraHandledException ex = new AuraHandledException(msg);
ex.setMessage(msg);
throw ex;
}
}
classes/VeryfiQRService.cls-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<status>Active</status>
</ApexClass>
classes/VeryfiWebhookEndpoint.cls
/**
* REST endpoint Veryfi POSTs to when a captured document finishes processing.
*
* Veryfi POSTs a minimal event payload ({event, data: {id}}). We fetch the full
* document via Veryfi's API, parse the originating Salesforce user id out of the
* external_id, and insert a Veryfi_Capture__c record owned by that user.
*
* Sharing: `without sharing` so we can set OwnerId at insert time regardless of
* the running user's sharing.
*/
@RestResource(urlMapping='/veryfi/webhook/*')
global without sharing class VeryfiWebhookEndpoint {
@HttpPost
global static void receive() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
res.addHeader('Content-Type', 'application/json');
try {
String body = req.requestBody != null ? req.requestBody.toString() : '';
Map<String, Object> payload = (Map<String, Object>) JSON.deserializeUntyped(body);
String eventName = (String) payload.get('event');
Map<String, Object> data = (Map<String, Object>) payload.get('data');
if (data == null || data.get('id') == null) {
res.statusCode = 200;
res.responseBody = Blob.valueOf('{"status":"ignored"}');
return;
}
String docId = String.valueOf(data.get('id'));
String prefix = (eventName != null && eventName.contains('.'))
? eventName.substringBefore('.')
: 'document';
Map<String, Object> doc = fetchDocument(prefix, docId);
String externalId = extractExternalId(doc);
Id ownerId = parseOwnerId(externalId);
if (ownerId == null) {
res.statusCode = 200;
res.responseBody = Blob.valueOf('{"status":"unparseable_external_id"}');
return;
}
Veryfi_Capture__c rec = new Veryfi_Capture__c(
External_Id__c = externalId,
OwnerId = ownerId,
Document_Id__c = docId,
Document_Type__c = prefix,
Document_Json__c = JSON.serialize(doc),
Status__c = 'Captured'
);
try {
insert rec;
} catch (DmlException dex) {
if (dex.getMessage().contains('DUPLICATE_VALUE')) {
res.statusCode = 200;
res.responseBody = Blob.valueOf('{"status":"already_captured"}');
return;
}
throw dex;
}
res.statusCode = 200;
res.responseBody = Blob.valueOf('{"status":"ok"}');
} catch (Exception e) {
res.statusCode = 500;
res.responseBody = Blob.valueOf('{"status":"error","message":' + JSON.serialize(e.getMessage()) + '}');
}
}
private static Map<String, Object> fetchDocument(String prefix, String docId) {
String endpoint = (prefix == 'document') ? 'documents' : 'any-documents';
HttpRequest httpReq = new HttpRequest();
httpReq.setEndpoint(VeryfiLensConfig.API_BASE_URL + '/api/v8/partner/' + endpoint + '/' + docId);
httpReq.setMethod('GET');
httpReq.setHeader('CLIENT-ID', VeryfiLensConfig.CLIENT_ID);
httpReq.setHeader('AUTHORIZATION', 'apikey ' + VeryfiLensConfig.USERNAME + ':' + VeryfiLensConfig.API_KEY);
httpReq.setTimeout(60000);
HttpResponse httpRes = new Http().send(httpReq);
if (httpRes.getStatusCode() < 200 || httpRes.getStatusCode() >= 300) {
throw new CalloutException('Veryfi document fetch ' + httpRes.getStatusCode() + ': ' + httpRes.getBody());
}
return (Map<String, Object>) JSON.deserializeUntyped(httpRes.getBody());
}
private static String extractExternalId(Map<String, Object> doc) {
if (doc.containsKey('meta') && doc.get('meta') instanceof Map<String, Object>) {
Object v = ((Map<String, Object>) doc.get('meta')).get('external_id');
if (v != null) return String.valueOf(v);
}
Object v2 = doc.get('external_id');
return v2 == null ? null : String.valueOf(v2);
}
private static Id parseOwnerId(String externalId) {
if (externalId == null || !externalId.startsWith('sf-')) {
return null;
}
String tail = externalId.substring(3);
Integer dashIdx = tail.indexOf('-');
if (dashIdx <= 0) {
return null;
}
String userIdStr = tail.substring(0, dashIdx);
try {
return Id.valueOf(userIdStr);
} catch (Exception e) {
return null;
}
}
}
classes/VeryfiWebhookEndpoint.cls-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<status>Active</status>
</ApexClass>
lwc/veryfiQrCapture/veryfiQrCapture.js
import { LightningElement } from 'lwc';
import generateQR from '@salesforce/apex/VeryfiQRService.generateQR';
import fetchCapture from '@salesforce/apex/VeryfiQRService.fetchCapture';
const POLL_INTERVAL_MS = 2000;
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
export default class VeryfiQrCapture extends LightningElement {
qrUrl;
linkUrl;
errorMsg;
isLoading = false;
externalId;
result;
pollTimer;
pollStartedAt;
get resultJson() {
return this.result ? JSON.stringify(this.result.document, null, 2) : '';
}
disconnectedCallback() {
this.stopPolling();
}
async handleGenerate() {
this.isLoading = true;
this.errorMsg = '';
this.qrUrl = '';
this.linkUrl = '';
this.result = null;
this.stopPolling();
try {
const data = await generateQR();
this.qrUrl = data.qrUrl;
this.linkUrl = data.linkUrl;
this.externalId = data.externalId;
this.startPolling();
} catch (e) {
this.errorMsg =
e?.body?.message ||
e?.body?.pageErrors?.[0]?.message ||
e?.body?.output?.errors?.[0]?.message ||
e?.message ||
JSON.stringify(e);
} finally {
this.isLoading = false;
}
}
handleReset() {
this.result = null;
this.qrUrl = '';
this.linkUrl = '';
this.externalId = null;
this.stopPolling();
this.handleGenerate();
}
handleCapture() {
if (this.linkUrl) {
window.open(this.linkUrl, '_blank', 'noopener');
}
}
startPolling() {
this.stopPolling();
this.pollStartedAt = Date.now();
this.pollTimer = setInterval(() => this.pollOnce(), POLL_INTERVAL_MS);
}
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
async pollOnce() {
if (!this.externalId) {
this.stopPolling();
return;
}
if (Date.now() - this.pollStartedAt > POLL_TIMEOUT_MS) {
this.stopPolling();
this.errorMsg = 'Timed out waiting for scan. Generate a new QR and try again.';
return;
}
try {
const record = await fetchCapture({ externalId: this.externalId });
if (!record) return;
this.result = {
id: record.Document_Id__c,
type: record.Document_Type__c,
document: JSON.parse(record.Document_Json__c || '{}')
};
this.qrUrl = '';
this.linkUrl = '';
this.stopPolling();
} catch (err) {
this.errorMsg =
'Failed to load captured document: ' +
(err?.body?.message || err?.message || 'unknown');
this.stopPolling();
}
}
}
lwc/veryfiQrCapture/veryfiQrCapture.html
<template>
<div class="container">
<h1>Scan Receipt</h1>
<template lwc:if={result}>
<div class="result-box">
<h2>Document received</h2>
<p>Document ID: {result.id}</p>
<pre class="result-json">{resultJson}</pre>
<lightning-button label="New Scan" variant="brand" onclick={handleReset}></lightning-button>
</div>
</template>
<template lwc:elseif={qrUrl}>
<p>Scan this QR code with your phone:</p>
<img src={qrUrl} class="qr-image" alt="Veryfi capture QR" />
<template lwc:if={linkUrl}>
<p>Or capture directly from this device:</p>
<lightning-button label="Capture" variant="brand" onclick={handleCapture}></lightning-button>
</template>
<p class="waiting">Waiting for capture...</p>
<br />
<lightning-button label="Generate New QR" variant="neutral" onclick={handleGenerate}></lightning-button>
</template>
<template lwc:elseif={isLoading}>
<p>Generating QR code...</p>
</template>
<template lwc:else>
<lightning-button label="Generate QR Code" variant="brand" onclick={handleGenerate} disabled={isLoading}></lightning-button>
</template>
<template lwc:if={errorMsg}>
<div class="error-box">{errorMsg}</div>
</template>
</div>
</template>
lwc/veryfiQrCapture/veryfiQrCapture.css
.container {
text-align: center;
padding: 40px;
font-family: Arial, sans-serif;
}
.container h1 {
font-size: 24px;
margin-bottom: 20px;
}
.qr-image {
width: 250px;
height: 250px;
border: 2px solid #ccc;
border-radius: 8px;
margin: 20px auto;
display: block;
}
.error-box {
background: #f8d7da;
padding: 15px;
border-radius: 6px;
margin-top: 20px;
}
.waiting {
color: #666;
font-style: italic;
margin-top: 10px;
}
.result-box {
background: #e3f1d5;
padding: 20px;
border-radius: 8px;
margin: 20px auto;
max-width: 720px;
}
.result-json {
background: #fff;
padding: 12px;
border-radius: 6px;
text-align: left;
max-height: 400px;
overflow: auto;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
}
lwc/veryfiQrCapture/veryfiQrCapture.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Veryfi QR</masterLabel>
<description>Generate a Veryfi capture QR code and display the OCR'd document</description>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
</LightningComponentBundle>
objects/Veryfi_Capture__c/Veryfi_Capture__c.object-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<enableActivities>false</enableActivities>
<enableBulkApi>true</enableBulkApi>
<enableHistory>false</enableHistory>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<enableSharing>true</enableSharing>
<enableStreamingApi>true</enableStreamingApi>
<label>Veryfi Capture</label>
<nameField>
<displayFormat>VC-{00000}</displayFormat>
<label>Capture Number</label>
<type>AutoNumber</type>
</nameField>
<pluralLabel>Veryfi Captures</pluralLabel>
<sharingModel>Private</sharingModel>
</CustomObject>
objects/Veryfi_Capture__c/fields/External_Id__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>External_Id__c</fullName>
<externalId>true</externalId>
<label>External Id</label>
<length>80</length>
<required>true</required>
<type>Text</type>
<unique>true</unique>
</CustomField>
objects/Veryfi_Capture__c/fields/Status__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Status__c</fullName>
<label>Status</label>
<required>false</required>
<type>Picklist</type>
<valueSet>
<restricted>true</restricted>
<valueSetDefinition>
<sorted>false</sorted>
<value>
<fullName>Pending</fullName>
<default>true</default>
<label>Pending</label>
</value>
<value>
<fullName>Captured</fullName>
<default>false</default>
<label>Captured</label>
</value>
<value>
<fullName>Failed</fullName>
<default>false</default>
<label>Failed</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>
objects/Veryfi_Capture__c/fields/Document_Id__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Document_Id__c</fullName>
<label>Document Id</label>
<length>50</length>
<required>false</required>
<type>Text</type>
</CustomField>
objects/Veryfi_Capture__c/fields/Document_Type__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Document_Type__c</fullName>
<label>Document Type</label>
<length>50</length>
<required>false</required>
<type>Text</type>
</CustomField>
objects/Veryfi_Capture__c/fields/Document_Json__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Document_Json__c</fullName>
<label>Document JSON</label>
<length>131072</length>
<type>LongTextArea</type>
<visibleLines>10</visibleLines>
</CustomField>
permissionsets/Veryfi_QR_Capture_User.permissionset-meta.xml
Assign to every end user who will use the QR component.
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<classAccesses>
<apexClass>VeryfiQRService</apexClass>
<enabled>true</enabled>
</classAccesses>
<fieldPermissions>
<editable>false</editable>
<field>Veryfi_Capture__c.Status__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Veryfi_Capture__c.Document_Id__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Veryfi_Capture__c.Document_Type__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Veryfi_Capture__c.Document_Json__c</field>
<readable>true</readable>
</fieldPermissions>
<hasActivationRequired>false</hasActivationRequired>
<label>Veryfi QR Capture User</label>
<objectPermissions>
<allowCreate>false</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>false</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Veryfi_Capture__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
</PermissionSet>
permissionsets/Veryfi_Webhook_Service.permissionset-meta.xml
Assign to the integration user Veryfi authenticates as for webhooks.
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<classAccesses>
<apexClass>VeryfiWebhookEndpoint</apexClass>
<enabled>true</enabled>
</classAccesses>
<fieldPermissions>
<editable>true</editable>
<field>Veryfi_Capture__c.Status__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Veryfi_Capture__c.Document_Id__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Veryfi_Capture__c.Document_Type__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Veryfi_Capture__c.Document_Json__c</field>
<readable>true</readable>
</fieldPermissions>
<hasActivationRequired>false</hasActivationRequired>
<label>Veryfi Webhook Service</label>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>false</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Veryfi_Capture__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
</PermissionSet>
remoteSiteSettings/Veryfi_Lens.remoteSite-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<RemoteSiteSetting xmlns="http://soap.sforce.com/2006/04/metadata">
<disableProtocolSecurity>false</disableProtocolSecurity>
<isActive>true</isActive>
<url>https://lens.veryfi.com</url>
</RemoteSiteSetting>
remoteSiteSettings/Veryfi_API.remoteSite-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<RemoteSiteSetting xmlns="http://soap.sforce.com/2006/04/metadata">
<disableProtocolSecurity>false</disableProtocolSecurity>
<isActive>true</isActive>
<url>https://api.veryfi.com</url>
</RemoteSiteSetting>
Org prerequisites
Before deploying, confirm the target org has:
- My Domain enabled (Setup → My Domain). Required for OAuth and the webhook URL.
- Lightning Experience enabled.
- API access at the org level (Enterprise/Unlimited; not all Developer Edition / Essentials orgs).
- An internal user with API Enabled on their profile to act as the webhook integration user. Customer Community profiles do not qualify.
Deploy
After copying the files above and setting credentials in VeryfiLensConfig.cls:
sf project deploy start --source-dir force-app/main/default
Production orgs require ≥75% Apex test coverage. Add a test class before production deploy. See Production deployment. Sandbox and Developer Edition orgs deploy without tests.
Assign permission sets
| Perm set | Assign to |
|---|---|
| Veryfi_QR_Capture_User | Every end user who will use the QR component |
| Veryfi_Webhook_Service | The integration user Veryfi authenticates as for webhooks |
The integration user's profile must also have API Enabled (profile-level; perm sets cannot grant this).
sf org assign permset --name Veryfi_QR_Capture_User --on-behalf-of [email protected]
sf org assign permset --name Veryfi_Webhook_Service --on-behalf-of [email protected]
Webhook authentication (Connected App + OAuth)
Salesforce REST endpoints require OAuth. Veryfi's webhook must send Authorization: Bearer <token>.
Salesforce: Setup → App Manager → New Connected App
- Name:
Veryfi Webhook - Enable OAuth Settings: ✓
- Callback URL:
https://localhost(required but unused) - OAuth Scopes:
Manage user data via APIs (api)+Perform requests at any time (refresh_token, offline_access) - Enable Client Credentials Flow: ✓
- Run As: the integration user with
Veryfi_Webhook_Service - Save and wait 2–10 minutes for propagation
- Name:
Copy the Connected App Consumer Key and Consumer Secret.
In the Veryfi dashboard, configure the webhook:
- Webhook URL:
https://<YOUR_MY_DOMAIN>.my.salesforce.com/services/apexrest/veryfi/webhook - OAuth token URL:
https://<YOUR_MY_DOMAIN>.my.salesforce.com/services/oauth2/token - Client ID / Client Secret: Consumer Key / Consumer Secret
- Grant type:
client_credentials
- Webhook URL:
Test: trigger a capture from the LWC, then verify:
sf data query --query "SELECT Name, External_Id__c, OwnerId, Status__c, CreatedDate FROM Veryfi_Capture__c ORDER BY CreatedDate DESC LIMIT 5"
Place the LWC on a page
Lightning Experience: Setup → Lightning App Builder → drag Veryfi QR from Custom onto a page → Save → Activate.
Experience Cloud: Experience Builder → drag Veryfi QR from Custom Components → Publish.
How it works
- User clicks Generate QR Code →
VeryfiQRService.generateQR()POSTs to/api/generate_qr. - User scans the QR or taps Capture → Veryfi's capture PWA opens on their phone.
- Veryfi POSTs to
/services/apexrest/veryfi/webhook→ endpoint fetches the document and insertsVeryfi_Capture__cowned by the QR-generating user. - The LWC polls
fetchCapture(externalId)every 2 seconds and renders the document JSON.
Privacy model
Veryfi_Capture__cuses private sharing — each record is owned by the user who generated the QR.VeryfiQRServiceiswith sharing;VeryfiWebhookEndpointiswithout sharing(insert-only, setsOwnerIdfromexternal_id).
Production deployment
Add an Apex test class with ≥75% coverage of VeryfiQRService and VeryfiWebhookEndpoint before deploying to production. Stub HTTP callouts with HttpCalloutMock and simulate REST requests via RestContext.
Customization
| What | Where |
|---|---|
| Page types | veryfiQrCapture.js-meta.xml <targets> |
| Polling interval / timeout | POLL_INTERVAL_MS, POLL_TIMEOUT_MS in veryfiQrCapture.js |
| Capture mode | 'mode' in VeryfiQRService.generateQR body (default 'document') — see QR Capture Guide |
| Structured fields | Parse Document_Json__c in a trigger or flow |
| Credentials | Move from VeryfiLensConfig.cls to Named Credential or Custom Metadata |
Troubleshooting
| Symptom | Fix |
|---|---|
Insufficient access rights on cross-reference id on generateQR | Assign Veryfi_QR_Capture_User |
Webhook 401 INVALID_SESSION_ID | Check Connected App Run As user, API Enabled, Client Credentials grant |
Webhook 403 INSUFFICIENT_ACCESS_OR_READONLY | Assign Veryfi_Webhook_Service to integration user |
| Records created but LWC never shows them | Sharing — confirm OwnerId matches polling user |
| Stuck on "Waiting for capture..." | Check Debug Logs; verify Veryfi webhook delivery |
Unauthorized endpoint from Apex | Deploy both Remote Site Settings |
Next steps
- QR Capture Guide — API reference and capture modes
- Configuration — SDK settings via QR
settingsobject - Demo Project — non-Salesforce Node.js example