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
- Install
- Platform setup
- Quick start
- How it works
- Configuration reference
- Callbacks & result
- Verification modules
- Environments
- Server-side verification
- Security notes
- Example app
- Versioning & web SDK parity
Features
- One call, typed end-to-end.
IDto.openreturns aFuture<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.opennew-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 landing —
IDtoLandingis a polished, drop-in verification screen (hero, steps, CTA, branding) that owns the token lifecycle and outcome handling. - Native bottom sheet —
displayMode: bottomSheetpresents a rounded sheet that slides up over a dim backdrop (tap-to-close).fullScreenis edge-to-edge. Those are the only two modes. - Session resume — pass a
sessionTokento 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.openthrowsStateError.
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. |
†
bottomSheetslides a rounded sheet up over a dim backdrop (tap-to-close);fullScreenis 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 tofull_screenon 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();
},
);
getTokenruns natively (it is never serialized into the WebView config) and answers the bundle over the existingidtobridge.- Refresh is single-flight and loop-safe (a same/empty token surfaces the
401instead of looping). Only401triggers it. - Omit
getTokenand behaviour is unchanged: an expired token surfaces viaonErrorwitherror: '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_secretin the app. Issue the short-livedclientTokenfrom 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.aidomain family (API, DigiLocker proxy, redirect targets), the CDN bucket, and DigiLocker by default. Add your own hosts withallowedHostsif a custombaseUrlor 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
completedresult.
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.