On-Device Face Capture
On-device Face Capture Guide
This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade.
Full rollout to all clients still TBD.
On-device face capture runs the full face-analysis pipeline (detection, positioning feedback, liveness, age estimation) inside the user's browser via WebAssembly. The captured selfie image never leaves the device — the SDK submits only the analysis results JSON to the server. Video recording is skipped on this path.
When to enable it
Reach for on-device face capture when:
- Privacy or data-residency posture requires keeping biometric image bytes off Incode's infrastructure.
- Bandwidth-sensitive environments (slow / unstable networks) make image upload unreliable but result JSON is small enough to send.
- Regulatory requirements bar the raw image from leaving the client.
If none of those apply, the server-side default is the recommended path — the backend pipeline has access to additional anti-spoof signals that aren't replicated on-device.
Requirements
Four things must line up. Missing any one of them and the path fails — usually at submission time, when the server rejects the request.
- E2EE configured at
setup()— the on-device face-results endpoint (/omni/add/face-results) is only served on the E2EE-provisioned host. You need both:encryption: true(or{ mgf1: 'sha256' }, whichever your environment expects)- The dedicated E2EE
apiURLIncode provisioned for your account - The
/0suffix onapiURLorcustomHeaders: { 'x-api-key': '<api key>' }so the tenant can be identified
See End-to-End Encryption for the provisioning dance.
2. WASM binary transport — implied by E2EE, but worth stating. The on-device pipeline runs inside the same WASM binary as the encrypted transport. setup({ wasm: false, ... }) is incompatible.
3. TheonDeviceSelfie WASM pipeline. Optional preload: pass wasm: { pipelines: ['selfie', 'onDeviceSelfie'] } to setup() so the models are warm before the user reaches the camera step. If you omit 'onDeviceSelfie', the SDK lazy-loads it on first camera open — fine for slow flows, noticeable for fast ones.
4. The config flag. Set onDeviceFaceResultsSubmissionEnabled: true on the Selfie or Authentication config:
const selfie = document.querySelector('incode-selfie');
selfie.config = {
onDeviceFaceResultsSubmissionEnabled: true,
// ...other SelfieConfig fields
};
Full Integration Example
Use the standard <incode-selfie> web component and set onDeviceFaceResultsSubmissionEnabled: true on its config. The component drives capture and submission; you only need to wire onFinish / onError.
Vanilla HTML / TypeScript
HTML
<incode-selfie></incode-selfie>
<script type="module">
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import '@incodetech/web/selfie';
import '@incodetech/web/selfie/styles.css';
// 1. Configure the SDK with E2EE + the on-device pipeline pre-warmed.
await setup({
apiURL: 'https://<your-incode-e2ee-host>/0', // E2EE host + /0 for JWT-derived API key
encryption: { mgf1: 'sha256' }, // pin SHA-256 when your E2EE env is provisioned for it
wasm: { pipelines: ['selfie', 'onDeviceSelfie'] },
});
// 2. Activate your session token.
await initializeSession({ token: 'your-session-token' });
// 3. Configure <incode-selfie> with the on-device flag enabled.
const selfie = document.querySelector('incode-selfie');
selfie.config = {
showTutorial: true,
showPreview: false,
assistedOnboarding: false,
enableFaceRecording: false, // ignored on the on-device path anyway
autoCaptureTimeout: 10,
captureAttempts: 3,
validateLenses: true,
validateFaceMask: true,
validateHeadCover: true,
validateClosedEyes: true,
validateBrightness: true,
deepsightLiveness: 'SINGLE_FRAME',
onDeviceFaceResultsSubmissionEnabled: true, // opt in
};
selfie.onFinish = () => console.log('On-device face capture complete');
selfie.onError = (err) => console.error('Selfie error:', err);
</script>
React
import { useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import type { SelfieConfig } from '@incodetech/core/selfie';
import '@incodetech/web/selfie';
import '@incodetech/web/selfie/styles.css';
type SelfieElement = HTMLElement & {
config?: SelfieConfig;
onFinish: () => void;
onError: (error: string) => void;
};
await setup({
apiURL: 'https://<your-incode-e2ee-host>/0',
encryption: { mgf1: 'sha256' }, // pin SHA-256 when your E2EE env is provisioned for it
wasm: { pipelines: ['selfie', 'onDeviceSelfie'] },
});
await initializeSession({ token: 'your-session-token' });
export function OnDeviceSelfie() {
const ref = useRef<SelfieElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
el.config = {
showTutorial: true,
showPreview: false,
assistedOnboarding: false,
enableFaceRecording: false,
autoCaptureTimeout: 10,
captureAttempts: 3,
validateLenses: true,
validateFaceMask: true,
validateHeadCover: true,
validateClosedEyes: true,
validateBrightness: true,
deepsightLiveness: 'SINGLE_FRAME',
onDeviceFaceResultsSubmissionEnabled: true,
};
el.onFinish = () => console.log('On-device face capture complete');
el.onError = (err) => console.error('Selfie error:', err);
}, []);
return <incode-selfie ref={ref} />;
}
What changes on this path
| Aspect | Default (server-side) | On-device |
|---|---|---|
| Image bytes leave the device | Yes — encrypted upload | No — only analysis results JSON |
Video recording (enableFaceRecording) |
Honored — local recording is assembled and uploaded if configured | Skipped — the recording service is never created |
| Required WASM pipelines | selfie |
selfie |
onDeviceSelfie |
||
| Setup complexity | Standard setup({ apiURL, token }) |
E2EE provisioning + dedicated apiURL |
/0 or x-api-key |
||
the onDeviceSelfie pipe |
Caveats
- Video recording is skipped. The recording service is never created on this path, so
enableFaceRecording: trueis silently ignored. If you need a local video artifact, the on-device path isn't compatible — use the server-side default or the capture-only flow. - Locked at boot with E2EE. You cannot flip between on-device and server-side at runtime in the same SDK boot — see End-to-End Encryption → Constraints to know about.