SDK reference · Android SDK

Android On Device Age Estimation

On-Device Age Estimation runs the face-analysis pipeline - face detection, positioning, liveness, and age estimation - entirely on the user's device during Face Capture (the SelfieScan module). On this path, no selfie image, video, or biometric frames are uploaded to Incode. Only structured analysis results (for example: liveness, face quality, and the estimated age) are submitted to the Incode backend. On-device processing requires E2EE: the SDK enforces it for the session and encrypts the submitted metadata client-side.

Because the biometric capture never leaves the device, the feature also skips the normal selfie image upload, video/frame recording upload, and manual capture. Age decisioning is performed on the backend from the submitted results.

Availability

On-Device Age Estimation is available on Android Welcome SDK 5.47.0 and later, in STANDARD mode only. It requires using UX V2, the SelfieScan module in SelfieScan.Mode.ENROLL, and is not supported for Face Login.

Prerequisites

This guide assumes the Welcome SDK is already installed and initializing in your app. If you are starting from scratch, complete Android SDK Setup (GitHub Packages, dependencies, permissions) and the SDK Integration guide first, then return here.

Before you enable On-Device Age Estimation, make sure the following are in place:

  • Android Welcome SDK 5.47.0 or later in SdkMode.STANDARD.
  • The two on-device model dependencies added to your app's module build.gradle / build.gradle.kts. These are optional for the Welcome SDK overall, but required for On-Device Age Estimation:
dependencies {
    // Required for On-Device Age Estimation
    implementation 'com.incode.sdk:model-liveness-detection'
    implementation 'com.incode.sdk:model-age-estimation'
}
dependencies {
    // Required for On-Device Age Estimation
    implementation("com.incode.sdk:model-liveness-detection")
    implementation("com.incode.sdk:model-age-estimation")
}

Versions are managed by the Incode BOM. See the BOM Version Mapping for the versions bundled with each SDK release. If either dependency is missing while the feature is enabled, SelfieScan.Builder.build() throws MissingModelLivenessDetectionDependencyException or MissingModelAgeEstimationDependencyException.

  • E2EE enabled for your organization and a valid E2EE URL. End-to-end encryption is required for on-device processing. If E2EE is not enabled on the session, the flow ends with OnDeviceProcessingNotCompatibleException (reason MISSING_E2EE). See the E2EE Integration Guide for the full setup.
  • A Flow configured in the Incode Dashboard with an accessible Flow ID (used as the configurationId on the session) if you use the server-driven startFlow() path.
  • No conflicting capture routing. Frame streaming and screen recording must be disabled on this path, because both would send frames off-device. See Error handling for the exact conditions.
  • The V2 (Compose) selfie UI. On-device processing runs only on the V2 selfie UI; a flow routed through the legacy V1 UI raises OnDeviceProcessingNotCompatibleException (reason V1_NOT_SUPPORTED). See Enable the V2 UI for how to turn it on per start method.

Minimum SDK

The model-age-estimation module targets the SDK's mandatory minSdk 23 and ships the same ABIs as the rest of the SDK, so it adds no new device or hardware gate for this feature. See Android SDK Setup for the supported ABIs and base packaging.

Configure On-Device Processing

For server-driven flows, log in to the Dashboard to enable On-Device Processing on the Face Capture module configuration in your Flow or Workflow.

  1. In the left menu, click Flow Builder > Flows OR Workflows.
  2. Open your Flow or Workflow.
  3. Open the configuration panel for the Face Capture module.
  4. Enable On-Device Processing. This ensures that liveness and age estimation run entirely on the user's device. No biometric images leave the SDK.
  5. If using Workflows, click Save configurations. Flow configurations save automatically.
  6. Once the configuration panel closes, if using Workflows, click Save & Publish. If using Flows, click Save Changes.

The SDK reads this setting from the flow automatically. There is no code flag to set.

Client-configured Flows (alternative)

If you build your own FlowConfig in code and start the flow with startOnboarding(), set the flag explicitly with SelfieScan.Builder().setOnDeviceFaceResultsSubmissionEnabled(true) (see step 3 below). The flag defaults to false.

Enable the V2 UI

On-device age estimation runs only on the V2 (Compose) selfie UI. A Flow on the legacy V1 UI raises OnDeviceProcessingNotCompatibleException (reason V1_NOT_SUPPORTED). Turn V2 on the way you would for any V2 integration. For the full mechanism and the ECSM caveat, see Enabling the V2 UI in the SDK Customization guide.

  1. In the left menu, click Flow Builder > Flows OR Workflows.
  2. Open your Flow or Workflow configured for on-device processing.
  3. Click the Settings tab.
  4. Toggle Enable new user experience on.
  5. For Flows, click Save Changes. For Workflows, click Save & Publish.

Client-configured Flows (alternative)

Enable V2 explicitly at initialization with IncodeWelcome.Builder.setClientExperimentId("experimentV2") (see step 1 below).

Integration steps

1. Initialize the SDK

Initialize the Welcome SDK, supplying your custom E2EE server URL as the fourth constructor argument, then obtain the singleton instance.

IncodeWelcome.Builder(this, WELCOME_API_URL, WELCOME_API_KEY, E2EE_URL)
    // Opts client-configured flows (startOnboarding) into UX V2 - see "Enabling the V2 UI".
    .setClientExperimentId("experimentV2")
    .build()

val incodeWelcome = IncodeWelcome.getInstance()
new IncodeWelcome.Builder(this, WELCOME_API_URL, WELCOME_API_KEY, E2EE_URL)
    // Opts client-configured flows (startOnboarding) into UX V2 - see "Enabling the V2 UI".
    .setClientExperimentId("experimentV2")
    .build();

IncodeWelcome incodeWelcome = IncodeWelcome.getInstance();

2. Create the session configuration

Create a SessionConfig that references your Dashboard Flow ID and enables E2EE. The configurationId is identical to the Dashboard flow ID (use "Copy flow ID" in the Dashboard).

val sessionConfig: SessionConfig = SessionConfig.Builder()
    .setConfigurationId(FLOW_ID)
    .setE2eEncryptionEnabled(true)
    .build()
SessionConfig sessionConfig = new SessionConfig.Builder()
    .setConfigurationId(FLOW_ID)
    .setE2eEncryptionEnabled(true)
    .build();

Because setE2eEncryptionEnabled() is a likely target for fraudsters, protect this code against reverse engineering. See the E2EE Integration Guide for the full hardening guidance.

3. Start the flow

Unlike iOS, Android does not use a presenting view controller. You pass an Android Context and the SDK launches its own activities.

Server-driven flow (recommended). With on-device processing enabled in the Dashboard, no code flag is needed. Start the flow with startFlow():

IncodeWelcome.getInstance().startFlow(
    activityContext,
    sessionConfig,
    onboardingListener)
IncodeWelcome.getInstance().startFlow(
    activityContext,
    sessionConfig,
    onboardingListener);

Server-driven workflow. Workflows are also server-driven. Enable "On-Device Processing" on the Face Capture step of your Workflow configuration in the Dashboard, set the session's configurationId to the Workflow ID, and start it with startWorkflow(). No code flag is needed:

IncodeWelcome.getInstance().startWorkflow(
    activityContext,
    sessionConfig,
    onboardingListener)
IncodeWelcome.getInstance().startWorkflow(
    activityContext,
    sessionConfig,
    onboardingListener);

Client-configured flow (alternative). If you build the FlowConfig in code, enable the feature on the SelfieScan module and start the flow with startOnboarding():

val flowConfig: FlowConfig = FlowConfig.Builder()
    // ... other modules
    .addSelfieScan(
        SelfieScan.Builder()
            .setOnDeviceFaceResultsSubmissionEnabled(true)
            .build())
    .build()

IncodeWelcome.getInstance().startOnboarding(
    activityContext,
    sessionConfig,
    flowConfig,
    onboardingListener)
FlowConfig flowConfig = new FlowConfig.Builder()
    // ... other modules
    .addSelfieScan(
        new SelfieScan.Builder()
            .setOnDeviceFaceResultsSubmissionEnabled(true)
            .build())
    .build();

IncodeWelcome.getInstance().startOnboarding(
    activityContext,
    sessionConfig,
    flowConfig,
    onboardingListener);

The SDK validates on-device compatibility at flow start on every path. If any requirement is not met, the flow ends via OnboardingListener.onError(...) (see Error handling).

4. Handle the result

The SelfieScanResult is delivered to your OnboardingListener through onSelfieScanCompleted(selfieScanResult). Errors are delivered asynchronously to onError(error).

val onboardingListener = object : IncodeWelcome.OnboardingListener {
    // ... other flow callbacks

    override fun onSelfieScanCompleted(selfieScanResult: SelfieScanResult) {
        // The selfie image and video were never uploaded to Incode on this path.
        // Continue your flow using the structured result.
    }

    override fun onError(error: Throwable) {
        if (error is OnDeviceProcessingNotCompatibleException) {
            // A requirement for on-device processing was not met.
        }
    }
}
IncodeWelcome.OnboardingListener onboardingListener = new IncodeWelcome.OnboardingListener() {
    // ... other flow callbacks

    @Override
    public void onSelfieScanCompleted(SelfieScanResult selfieScanResult) {
        // The selfie image and video were never uploaded to Incode on this path.
        // Continue your flow using the structured result.
    }

    @Override
    public void onError(Throwable error) {
        if (error instanceof OnDeviceProcessingNotCompatibleException) {
            // A requirement for on-device processing was not met.
        }
    }
};

Result on the on-device path

Because no server-side face evaluation runs on this path, SelfieScanResult.isSpoofAttempt stays null. The estimated age is computed on-device and submitted to Incode as part of the structured results; it is not returned in SelfieScanResult.

The SelfieScanResult still contains the locally captured selfie image (selfieBase64 / fullFrameSelfieBase64, and the local croppedSelfieImgPath / fullFrameSelfieImgPath). The on-device guarantee is that these image bytes are never uploaded to Incode - not that the local result is image-less. Handle them on-device according to your own privacy requirements.

Compatible configuration matrix

On-device age estimation only works in a narrow, privacy-preserving configuration. Unusual Flow / Workflow / FlowConfig combinations that are valid for a normal Face Capture will abort the session when on-device processing is enabled. Use this matrix when configuring any on-device age-estimation integration.

Setting Required value Where to set it If wrong
SDK mode SdkMode.STANDARD App init Feature unavailable
Welcome SDK version 5.47.0+ App dependencies / BOM Feature unavailable
Model artifacts model-liveness-detection and model-age-estimation present App build.gradle MissingModel*DependencyException at SelfieScan.Builder.build()
E2EE Enabled on the session + valid E2EE URL at SDK init SessionConfig.setE2eEncryptionEnabled(true) + 4-arg IncodeWelcome.Builder OnDeviceProcessingNotCompatibleException (MISSING_E2EE)
On-Device Processing On for the Face Capture module Dashboard Face Capture config, or SelfieScan.Builder().setOnDeviceFaceResultsSubmissionEnabled(true) for client-built flows On-device path not entered
UX V2 On ("Enable new user experience") Dashboard Flow/Workflow Settings, or setClientExperimentId("experimentV2") for client-built flows OnDeviceProcessingNotCompatibleException (V1_NOT_SUPPORTED)
Selfie mode SelfieScan.Mode.ENROLL Face Capture / SelfieScan config Face Login / auth modes are not supported
Frame streaming / "Enable face recording" Off Dashboard Face Capture, or do not call setStreamFramesEnabled(true) OnDeviceProcessingNotCompatibleException (FRAME_STREAMING)
Screen recording Off FlowConfig.Builder.configureScreenRecording(false, ...) OnDeviceProcessingNotCompatibleException (SCREEN_RECORDING)
Manual capture Disabled by the SDK on this path N/A (not configurable when on-device is on) Manual-capture UI / timeout fallbacks do not apply; keep auto-capture healthy
Start path startFlow / startWorkflow (recommended) or startOnboarding with the flag above App code Same compatibility checks run on every path

Expected differences vs a normal Face Capture

On this path the SDK does not upload the selfie image or video, does not expose manual capture, and does not run server-side face evaluation (SelfieScanResult.isSpoofAttempt stays null). Server-side checks that depend on a stored selfie (for example Face Recognition / Face Match scores) may return N/A even if those modules appear later in the same Flow or Workflow. Configure age decisioning from the on-device structured results your backend receives, not from selfie-dependent scores.

Error handling

Two distinct failures can occur when the feature is misconfigured:

  • MissingModelLivenessDetectionDependencyException / MissingModelAgeEstimationDependencyException - thrown synchronously from SelfieScan.Builder.build() when the feature is enabled but the required optional model dependency is absent. Fix by adding the dependencies from Prerequisites.
  • OnDeviceProcessingNotCompatibleException - delivered asynchronously to OnboardingListener.onError(...) at flow start when the configuration is incompatible. It carries one of the following reasons:
Reason Meaning Fix
MISSING_E2EE E2EE is not enabled on the session. Call SessionConfig.Builder.setE2eEncryptionEnabled(true).
FRAME_STREAMING Frame streaming is enabled on the same SelfieScan. Disable frame streaming (do not call setStreamFramesEnabled(true) / face recording).
SCREEN_RECORDING Screen recording is configured for the flow. Call FlowConfig.Builder.configureScreenRecording(false, ...).
V1_NOT_SUPPORTED The flow routes through the legacy V1 selfie UI (for example Face Login). Use the V2 selfie UI in enrollment mode.
Was this page helpful?