On-Device Age Estimation
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.
Opt-in. Default Selfie / Authentication runs server-side face analysis, where the captured frame is encrypted and uploaded for the backend pipeline to score.
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.
- 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. - The
onDeviceSelfieWASM pipeline. Optional preload: passwasm: { pipelines: ['selfie', 'onDeviceSelfie'] }tosetup()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. - The config flag. Set
onDeviceFaceResultsSubmissionEnabled: trueon 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
<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
React 18 or earlier: add the one-time JSX augmentation from Framework Integration → TypeScript: JSX support for
incode-*tags so TypeScript recognizes the Incode tags. The example below uses a ref +useEffectto assignconfig/onFinish/onErrorbecause JSX attributes only accept strings. React 19+ doesn't need this — see Framework Integration → React 19+ shortcut for the simpler inline-prop form.
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} />;
}Inside
<incode-flow>? When<incode-selfie>runs as part of<incode-flow>(orcreateOrchestratedFlowManager), the orchestrator passes the dashboard-configuredFlowModuleConfig['SELFIE']to the component automatically — you do not setselfie.configyourself. EnableonDeviceFaceResultsSubmissionEnabledon the SELFIE module configuration in the Incode dashboard instead, and the flag flows through.
Going fully custom UI, or using Authentication? The same
onDeviceFaceResultsSubmissionEnabled: trueflag works on the headless managers. See Headless Mode.
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.
See also
- End-to-End Encryption — required prerequisite.
- Module: Selfie — config catalogue and full Selfie reference.
- Module: Authentication — same flag, returning-user re-auth.
- WASM Configuration — the
onDeviceSelfiepipeline entry. - Headless Mode — driving the Selfie / Authentication managers from custom UI.
