idto_flutter

Add IDto identity verification (KYC) to your Flutter app. This package runs the official IDto web SDK CDN bundle inside an in-app WebView and exposes a single, typed, imperative call:

final IDtoResult result = await IDto.open(context, config: IDtoConfig(...));

The web SDK is never forked. The wrapper loads the same rolling CDN bundle the web integration uses (selected by env), so new web SDK releases — new modules, fixes, UI — flow through automatically. This package only owns the typed Dart config surface, the callback bridge, and the native WebView host (camera/mic permissions, DigiLocker redirects, a navigation allow-list).

Features

  • One call, typed end-to-end. IDto.open returns a Future<IDtoResult>; every config field and callback payload is a typed Dart class — no stringly maps.
  • Full module coverage via the web SDK: mobile OTP, Aadhaar (DigiLocker / OKYC), PAN, face match + liveness, bank account, driving licence, vehicle RC, e-sign, and silent name match. See Verification modules.
  • Camera & microphone handled natively for document capture and liveness.
  • DigiLocker OAuth — both same-window redirects and window.open new-window flows complete inside the WebView and return to the session.
  • Native report sharing — the result PDF is captured from the WebView and handed to the OS share/save sheet (via share_plus). See Report download.
  • Batteries-included landingIDtoLanding is a polished, drop-in verification screen (hero, steps, CTA, branding) that owns the token lifecycle and outcome handling.
  • Native bottom sheetdisplayMode: bottomSheet presents a rounded sheet that slides up over a dim backdrop (tap-to-close). fullScreen is edge-to-edge. Those are the only two modes.
  • Session resume — pass a sessionToken to continue an interrupted flow.
  • Branding & theming — colors, logo, light/dark, English/Hindi.
  • Hardened host — an in-WebView navigation allow-list (IDto API + CDN + DigiLocker only, extensible), a configurable ready-timeout, and deterministic init-failure handling.
  • Single-session guard — a second concurrent IDto.open throws StateError.

Install

dependencies:
  idto_flutter: ^0.3.0

This package depends on flutter_inappwebview (the WebView host). No other setup beyond the platform permissions below.

Required platform setup

Camera (and microphone, if your workflow records a liveness step) permissions must be declared in the host app — a package cannot edit them for you.

iOS — add to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera access is required to verify your identity.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for the liveness check.</string>

Android — add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

A real device is required for end-to-end face match — the iOS Simulator and most Android emulators have no usable camera.

Quick start

import 'package:idto_flutter/idto_flutter.dart';

// `clientToken` is short-lived and comes from YOUR backend, which exchanges
// your client_id/client_secret at POST /auth/sdk/token. Never embed the secret
// in the app (see Security notes).
final IDtoResult result = await IDto.open(
  context,
  config: IDtoConfig(
    clientToken: token,
    workflowTemplateId: 'your-workflow-uuid',
    businessName: 'Acme Lending',
    env: IDtoEnv.production, // default
    theme: IDtoTheme.light,
  ),
  onStepComplete: (d) => debugPrint('step ${d.step} done'),
  onError: (d) => debugPrint('error at ${d.step}: ${d.error}'),
  onAbandon: (d) => debugPrint('abandoned at ${d.atStep}'),
  // Recommended: auto-refresh the bearer when it expires mid-session.
  getToken: () => fetchClientToken(), // your server-side mint
);

switch (result.status) {
  case IDtoStatus.completed:
    // Confirm result.sessionToken on your backend, then grant access.
    break;
  case IDtoStatus.closed:
  case IDtoStatus.abandoned:
  case IDtoStatus.error:
    break;
}

IDto.open pushes a full-screen route, resolves on the terminal event, and pops its route automatically. Calling it again while a flow is active throws a StateError.

How it works

Your backend                 Your Flutter app                 IDto
────────────                 ────────────────                 ────
client_id + secret
   │ POST /auth/sdk/token
   └───────────────────────────────────────────────────────────▶ client_token
                                   │ IDto.open(config: clientToken …)
                                   │   loads CDN bundle in WebView
                                   │   runs the workflow's modules
                                   │◀── onStepComplete (per step)
                                   │◀── onError / onAbandon
                                   ▼
                             IDtoResult (completed / closed / abandoned / error)
   GET /sdk/v2/session/<token>/state  ◀── confirm server-side before granting access

The on-device result drives UX; your backend confirms the truth via the state API or a webhook (see Server-side verification).

Configuration reference

IDtoConfig mirrors the web SDK config 1:1 with typed enums. Only clientToken and workflowTemplateId are required.

Core

Dart field Wire key Description
clientToken client_token Required. Short-lived token from your backend's /auth/sdk/token.
workflowTemplateId workflow_template_id Required. Workflow UUID from the IDto dashboard.
env env IDtoEnv.production (default) or IDtoEnv.development. Selects CDN bundle + API base.
baseUrl baseUrl Advanced: override the IDto API base URL.

User & session

Dart field Wire key Description
merchantUserId merchant_user_id Your internal user id (shown in the dashboard).
phone phone Pre-fills the mobile OTP step and de-duplicates sessions.
sessionToken session_token Resume an in-progress session.
startFresh start_fresh Force a new session even if one exists.
preVerified pre_verified Map<String,bool> of module slugs already verified, to skip those steps.
accumulatedData accumulated_data Prior-session data to rehydrate (merchant_injects mode).
referenceName reference_name Reference name for the silent name_match module.

Branding & display

Dart field Wire key Description
businessName businessName Shown in the widget header.
logo logo Absolute HTTPS logo URL.
theme theme IDtoTheme.light (default) / IDtoTheme.dark.
language language IDtoLanguage.en (default) / IDtoLanguage.hi.
displayMode displayMode IDtoDisplayMode.fullScreen (default) / IDtoDisplayMode.bottomSheet. †
colors colors IDtoColors overrides: background, text, text2, border, primary, secondary, buttonTextColorPrimary, buttonTextColorSecondary.
bottomSheet bottomSheet IDtoBottomSheet(minHeight, maxHeight) — sheet height for bottomSheet mode. minHeight accepts a num (px) or String percentage (e.g. '60%'); default 90%.
desktopModal desktopModal Web-contract field only — kept for parity with the web SDK config; it drives no native UI in the Flutter SDK.

bottomSheet slides a rounded sheet up over a dim backdrop (tap-to-close); fullScreen is edge-to-edge. Those are the only two native modes — the web SDK's responsive desktop modal is not used on a phone. The web SDK is always forced to full_screen on the wire so it fills the native surface and never draws a nested drawer inside it.

Module configuration

Dart field Wire key Description
aadhaarConfig aadhaarConfig IDtoAadhaarConfig(digilockerMaxFailures, okycEnabled). okycEnabled is off by default (compliance — enable only with IDto approval).
panConfig panConfig IDtoPanConfig(skipContextScreen) — skip the PAN consent screen (you collect consent yourself).
faceMatchConfig faceMatchConfig IDtoFaceMatchConfig(skipLiveness, threshold, livenessFailurePolicy). See below.
faceMatchReferenceImage faceMatchReferenceImage HTTPS URL or base64 data URI to match the captured face against.
nameMatchConfig nameMatchConfig IDtoNameMatchConfig(threshold, decisionMode, extraHonorifics, aliases, comparePairs) — rules for the silent name match.

IDtoFaceMatchConfig

Field Default (web SDK) Description
skipLiveness true When true, only the face-comparison API is called; liveness is skipped.
threshold 70 Minimum match percentage (0–100) to count as a match.
livenessFailurePolicy needsReview What to do when the liveness verdict is unavailable (only applies when skipLiveness: false). IDtoLivenessFailurePolicy.needsReview blocks the auto-pass and routes to manual review; failClosed treats it as a failure; failOpen continues anyway (unsafe — opt-in only). An explicit not-live verdict always blocks.

Host-only (Dart) options

These tune the native WebView host and are not sent to the web SDK.

Dart field Default Description
debug false Forward in-WebView console logs and blocked-navigation notices to debugPrint.
readyTimeout 30s How long to wait for the bundle to signal ready before failing with a network_error.
allowedHosts [] Extra hostnames the WebView may navigate to, on top of the IDto API host, the CDN bucket, and DigiLocker.

Callbacks & result

IDto.open takes three optional per-event callbacks and resolves to an IDtoResult on the terminal event.

// Per step:
onStepComplete:     (IDtoStepData d) { d.step; d.result; d.accumulatedData;
                                       d.sessionToken; d.creditsDeducted; d.balanceRemaining; }
// All steps done — fired while the web success / download-report screen is still
// showing. NOT terminal: the Future resolves later, on close (see below).
onWorkflowComplete: (IDtoWorkflowCompleteData d) { d.allSteps; d.accumulatedData; d.sessionToken; }
onError:            (IDtoErrorData d) { d.step; d.error; d.sessionToken; }
onAbandon:          (IDtoAbandonData d) { d.atStep; d.reason; d.sessionToken; }
class IDtoResult {
  final IDtoStatus status;          // completed | closed | abandoned | error
  final String sessionToken;        // confirm this server-side
  final Map<String, dynamic> accumulatedData;
  final IDtoErrorData? error;       // non-null only when status == error
}
IDtoStatus Meaning
completed All workflow steps finished AND the user then closed the widget. Confirm sessionToken server-side. (onWorkflowComplete fires earlier, when the steps finish.)
closed User dismissed the widget (or hardware back) without completing.
abandoned User abandoned mid-flow (onAbandon fired first).
error A fatal error halted the flow (init failure or insufficient_credits); result.error is populated.

Non-fatal errors fire onError but do not resolve the Future — the flow can recover and continue. onWorkflowComplete is not terminal either: the web SDK keeps showing its success / download-report screen after the steps finish, and the Future resolves (with IDtoStatus.completed) only when the user then closes.

IDtoLanding (batteries-included screen)

IDtoLanding is a drop-in full-screen verification landing — a polished hero, numbered steps, CTA, trust row, and footer — that mints the token via getToken, opens the flow, and reports the outcome. You supply getToken, workflowTemplateId, and a little branding; the SDK owns the rest.

IDtoLanding(
  getToken: () => fetchClientToken(),          // your server-side mint
  workflowTemplateId: 'your-workflow-uuid',
  businessName: 'Acme Lending',
  brandColor: '#0019FF',                        // drives CTA + palette
  logo: const AssetImage('assets/logo.png'),    // native landing header
  logoUrl: 'https://acme.example/logo.png',     // in-flow web header
  onComplete: (d) => debugPrint('done ${d.sessionToken}'),
  onAbandon: (d) => debugPrint('abandoned ${d.atStep}'),
  onDismiss: () => debugPrint('closed'),
);

Override any text via copy: IDtoLandingCopy(heroTitle: '…', steps: ['…']) (unspecified fields keep the defaults; pass ''/const [] to hide an element). All verification config (env, baseUrl, faceMatchConfig, language, theme, …) is accepted directly and forwarded to the flow.

Report download

When a workflow produces a downloadable PDF report, the web SDK builds an <a download> with a blob: URL — which a WebView cannot save on its own. The SDK intercepts that blob, base64-marshals it to Dart, and hands the file to the OS share/save sheet (via share_plus, staged in the temp dir and deleted right after). It is fully automatic — no API to call — and a failure (or the user dismissing the sheet) is swallowed and never interrupts the KYC session.

Token auto-refresh

The clientToken is short-lived (~15 minutes). Without auto-refresh, an expired token strands the user mid-flow until you re-open with a new token. Pass a getToken callback and the SDK recovers transparently: when the in-WebView bundle hits a 401, it asks the host for a fresh token, the SDK calls your getToken natively, and retries the failed request once.

await IDto.open(
  context,
  config: IDtoConfig(clientToken: token, workflowTemplateId: '…'),
  getToken: () async {
    // Mint server-side; never embed client_secret in the app.
    return await fetchClientToken();
  },
);
  • getToken runs natively (it is never serialized into the WebView config) and answers the bundle over the existing idto bridge.
  • Refresh is single-flight and loop-safe (a same/empty token surfaces the 401 instead of looping). Only 401 triggers it.
  • Omit getToken and behaviour is unchanged: an expired token surfaces via onError with error: 'session_expired'.

Verification modules

The workflow you reference by workflowTemplateId decides which of these run and in what order (configured in the IDto dashboard, not in the app):

Module Slug What the user does
Mobile OTP mobile_verification Enters phone, verifies an OTP.
Aadhaar aadhar_verification DigiLocker OAuth (or OKYC / upload fallback).
PAN pan_verification Enters PAN; verified against source.
Face match face_match Captures a selfie; optional liveness.
Bank account account_verification Verifies a bank account / IFSC.
Driving licence driving_licence Verifies a DL.
Vehicle RC vehicle_rc Verifies a vehicle RC.
E-sign e_sign Signs a document.
Govt ID selection govt_id_selection Chooses an ID method (router step).
Name match name_match Silent — compares names across sources; never blocks the user, returns scores.

Environments

env CDN bundle API base
IDtoEnv.production* …/sdk/prod/idto.js https://prod.idto.ai
IDtoEnv.development …/sdk/dev/idto.js https://dev.idto.ai

* default. The web SDK maps env → API base internally; baseUrl overrides it.

Server-side verification

Always confirm the outcome on your backend before granting access — never trust the on-device result alone:

GET https://prod.idto.ai/sdk/v2/session/<sessionToken>/state
Authorization: Bearer <IDTO_API_SECRET>

session.status is completed | in_progress | abandoned | expired. Prefer the signed webhook (configured in the dashboard) where available.

Security notes

  • Never ship client_secret in the app. Issue the short-lived clientToken from your backend. The bundled example inlines a secret only because it talks to a throwaway dev sandbox.
  • Navigation is allow-listed. The WebView only navigates to the idto.ai domain family (API, DigiLocker proxy, redirect targets), the CDN bucket, and DigiLocker by default. Add your own hosts with allowedHosts if a custom baseUrl or redirect needs them.
  • Same-origin. The WebView runs under the IDto API origin, so the bundle's API calls need no CORS exception.
  • Confirm server-side (above) before acting on a completed result.

Example app

example/ is a one-tap runnable app: it fetches a clientToken from the IDto dev sandbox, opens a real verification session, and streams every callback into an on-screen event log.

cd example
flutter pub get
flutter run        # use a real device for camera-based steps

Set your sandbox values in example/lib/dev_credentials.dart first — see example/README.md for how the dev credentials are provisioned and how to point the example at a local backend instead.

Versioning & web SDK parity

This wrapper's typed config is kept in lock-step with the web SDK contract (web_sdk/src/cdn/global.d.ts). A guard test (test/parity_test.dart) fails CI if the web SDK gains a config field or enum value this package has not mirrored, so the Dart surface cannot silently drift. See CHANGELOG.md.

Libraries

idto_flutter
IDto Flutter SDK.