@idto/react-native-sdk
v0.2.4
Published
React Native wrapper for the IDto identity verification web SDK (CDN bundle in a WebView).
Maintainers
Readme
@idto/react-native-sdk
Add IDto identity verification (KYC) to your React Native or Expo app. The SDK handles the entire flow — mobile OTP, Aadhaar, PAN, face match + liveness, bank verification, and more — in the exact order your workflow defines. Your users get a guided, branded experience; you get verified results via typed callbacks.
Under the hood it runs the official IDto web SDK CDN bundle (idto.js) inside
a react-native-webview
and exposes two components:
// Low-level: you own the screen, the trigger, and `visible`.
<IDtoVerification visible clientToken={token} workflowTemplateId="…" … />
// Batteries-included: a polished full landing screen + token lifecycle + sheet.
<IDtoLanding getToken={mintToken} workflowTemplateId="…" brandColor="#0019FF" … />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 React
props surface, the callback bridge, and the native WebView host (camera / mic
permissions, DigiLocker redirects, a navigation allow-list, native report sharing).
Unlike the web integration, there is no Content-Security-Policy to configure
and no CORS exception to add — the WebView runs same-origin under the IDto API
host (see How it works).
- What you need
- Features
- Install
- Platform setup
- Quick start (
IDtoVerification) - Quick start (
IDtoLanding) - How it works
- Props reference
IDtoLandingreference- Token auto-refresh
- Callbacks & data shapes
- Step results
- Verification outcome
- Credits
- Resuming a session
- Pre-filling data
- Verification modules
- Environments
- Receiving results (state API & signed webhook)
- Session statuses
- Security notes
- DigiLocker
- E-sign
- TypeScript
- Patterns
- Troubleshooting
- Example apps
What you need
To integrate the SDK you need:
- An IDto account and API secret (
IDTO_API_SECRET). Sign up or contact the team at idto.ai to get credentials. - A workflow template UUID (
workflowTemplateId). IDto creates this during onboarding and gives you the UUID — it decides which modules run and in what order. - A backend endpoint that mints a short-lived
clientTokenfrom yourclient_id/client_secret. The secret must never ship in the app. - A sandbox environment for development — set
env="development"(points athttps://dev.idto.ai). Ask your IDto integration manager for a sandbox secret and test credentials. - A real device for end-to-end face match / document capture — the iOS Simulator and most Android emulators have no usable camera.
Features
- Two components, typed end-to-end.
IDtoVerification(low-level Modal) andIDtoLanding(a complete, branded landing screen). Every config field is a camelCase prop and every callback payload is a typed interface — 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, govt-ID selection, and silent name match. See Verification modules.
- Camera & microphone handled natively for document capture and liveness —
inline
getUserMediaworks inside the WebView with the right permissions. - DigiLocker OAuth — both same-window redirects and
window.opennew-window flows complete in-app and return to the session (same-origin, no system-browser hop). - Native report sharing — the result PDF is captured from the WebView and handed to the OS share/save sheet (optional native modules).
- Session resume — pass a
sessionTokento continue an interrupted flow. - Token auto-refresh — supply
getTokenand an expired bearer is replaced transparently mid-session; the user sees nothing. - Branding & theming — colors, logo, light/dark, English/Hindi, full-screen or bottom-sheet display, configurable sheet / desktop-modal sizing.
- Hardened host — an in-WebView navigation allow-list (IDto API + CDN +
DigiLocker only, extensible via
allowedHosts), a configurablereadyTimeout, and deterministic init-failure handling.
Install
npm install @idto/react-native-sdk react-native-webview react-native-safe-area-context
# iOS (bare RN) — install the native pods:
cd ios && pod install && cd ..react, react-native, react-native-webview, and react-native-safe-area-context are peer dependencies:
| Peer | Version |
| --------------------- | ---------- |
| react | >= 18.0.0 |
| react-native | >= 0.71.0 (or Expo SDK 49+) |
| react-native-webview| >= 13.6.0 — minimum recommended for inline camera (getUserMedia) and inline media playback on iOS/Android. Pin the exact version you confirm on device. |
| react-native-safe-area-context| >= 4.0.0 — supplies real WindowInsets so the status bar, notch, home indicator, and Android nav/gesture bar are padded correctly (including Android edge-to-edge). Most RN apps (anything using React Navigation) already have it. The SDK renders its own SafeAreaProvider inside the verification Modal, so you do not need to wrap your app — only install the package. |
Optional: report download
The verification flow can let the user download the result report (PDF). Inside a
WebView the browser's own <a download> does nothing, so the SDK captures the
file and hands it to the native share/save sheet — but only if these two
optional native modules are installed. Without them the download is a
no-op (one console warning), and the rest of the flow is unaffected.
npm install react-native-blob-util react-native-share
# iOS (bare RN) — reinstall pods, then rebuild the app:
cd ios && pod install && cd ..| Optional peer | Version | Purpose |
| ------------------------ | ----------- | ------- |
| react-native-blob-util | >= 0.19.0 | Stages the captured PDF in the app cache. |
| react-native-share | >= 10.0.0 | Presents the OS share sheet (Save to Files / share / print). |
A real device is required for end-to-end face match / document capture — the iOS Simulator and most Android emulators have no usable camera.
Platform setup
The flow needs camera access (and microphone if your workflow records a liveness step). These permissions must be declared in the host app.
Expo (managed) — config plugin
Add the bundled config plugin to app.json / app.config.js, then prebuild:
{ "expo": { "plugins": ["@idto/react-native-sdk"] } }npx expo prebuildThe plugin (app.plugin.js) adds, automatically:
- iOS —
NSCameraUsageDescriptionandNSMicrophoneUsageDescription. - Android —
android.permission.CAMERAandandroid.permission.RECORD_AUDIO.
(Existing values you set yourself are preserved.)
Bare React Native — manual permissions
iOS — add to
ios/<App>/Info.plist:<key>NSCameraUsageDescription</key> <string>We use the camera to verify your identity (face match and document capture).</string> <key>NSMicrophoneUsageDescription</key> <string>We use the microphone during liveness checks.</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" />
react-native-webview auto-grants the WebView's media request once the OS-level
permission above is held — no extra wiring needed.
Quick start (IDtoVerification)
Use IDtoVerification when you own the trigger and the surrounding screen.
Step 1 — mint a client token on your backend
The 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).
// Your server (Node/Express shown). Authenticate the user first.
app.post('/api/idto-token', async (req, res) => {
const r = await fetch('https://prod.idto.ai/auth/sdk/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + process.env.IDTO_API_SECRET,
},
body: JSON.stringify({ user_id: req.user.id }), // optional but recommended for audit trails
})
const { client_token } = await r.json()
res.json({ client_token })
})Step 2 — render the component
import { useState, useCallback } from 'react'
import { Button } from 'react-native'
import { IDtoVerification } from '@idto/react-native-sdk'
function KycScreen() {
const [visible, setVisible] = useState(false)
const [clientToken, setClientToken] = useState<string | null>(null)
const start = useCallback(async () => {
const r = await fetch('https://your-backend.example/api/idto-token', { method: 'POST' })
setClientToken((await r.json()).client_token)
setVisible(true)
}, [])
return (
<>
<Button title="Verify your identity" onPress={start} />
{clientToken && (
<IDtoVerification
visible={visible}
clientToken={clientToken}
workflowTemplateId="your-workflow-uuid"
businessName="Acme Lending"
env="development" // 'production' (default) | 'development'
theme="light"
onStepComplete={(d) => console.log('step', d.step, 'done')}
onWorkflowComplete={(d) => {
setVisible(false)
// POST d.session_token to your backend to confirm server-side.
}}
onError={(d) => console.error('IDto error @', d.step, d.error)}
onAbandon={() => setVisible(false)}
onClose={() => setVisible(false)}
/>
)}
</>
)
}The component owns
visible. Flip ittrueto start, and set itfalseinonWorkflowComplete/onClose/onAbandonto tear the widget down. Config props are read once per open and are immutable for that session — close and reopen to apply new config. Wait foronClosebefore reopening (a close handshake may still be in flight for ~1s).
Omitting
onErrororonAbandonsilently swallows failures. Always wire at least aconsole.errorstub during development so you can see what went wrong.
Step 3 — confirm server-side
app.post('/api/kyc/complete', async (req, res) => {
const { session_token } = req.body
const r = await fetch(
'https://prod.idto.ai/sdk/v2/session/' + session_token + '/state',
{ headers: { Authorization: 'Bearer ' + process.env.IDTO_API_SECRET } },
)
if (!r.ok) return res.status(400).json({ error: 'verification_failed', status: r.status })
const session = await r.json()
// Gate on the verdict, not merely on "finished":
if (session.status === 'completed' && session.outcome === 'verified') {
// unlock downstream logic
}
res.json({ ok: true })
})See Receiving results for the authoritative webhook path.
Quick start (IDtoLanding)
Use IDtoLanding when you want a complete, polished verification screen with
zero layout work. You supply getToken, workflowTemplateId, and branding; the
SDK owns the landing UI (header, hero, steps, CTA, trust, footer), the token
lifecycle, the sheet presentation, safe areas, and outcome handling.
import { IDtoLanding } from '@idto/react-native-sdk'
function VerifyScreen() {
return (
<IDtoLanding
getToken={async () => {
const r = await fetch('https://your-backend.example/api/idto-token', { method: 'POST' })
return (await r.json()).client_token
}}
workflowTemplateId="your-workflow-uuid"
brandColor="#0019FF"
logo={require('./assets/logo.png')} // native landing-header image
businessName="Acme Lending"
env="development"
copy={{ heroTitle: 'Verify to continue' }} // override any landing copy
onComplete={(d) => {/* POST d.session_token to confirm server-side */}}
onDismiss={() => navigation.goBack()}
/>
)
}Everything IDtoVerification accepts (except the props the landing owns — see
IDtoLanding reference) is inherited and forwarded to
the sheet verbatim, so aadhaarConfig, faceMatchConfig, theme, language,
phone, etc. all work here too.
How it works
Your backend Your RN/Expo app IDto
──────────── ──────────────── ────
client_id + secret
│ POST /auth/sdk/token
└───────────────────────────────────────────────────────────▶ client_token
│ <IDtoVerification clientToken … />
│ loads CDN bundle in a WebView
│ runs the workflow's modules
│◀── onStepComplete (per step)
│◀── onWorkflowComplete / onError / onAbandon
▼
onClose (widget torn down)
GET /sdk/v2/session/<token>/state ◀── confirm server-side before granting access- Your backend mints a short-lived
client_tokenfrom/auth/sdk/tokenusing yourclient_id/client_secret. The secret never leaves the server. - Pass that token as the component's
clientTokenprop and setvisible. - The component streams
onStepCompleteper step and a terminalonWorkflowComplete/onError/onAbandon, thenonClose. - On
onWorkflowComplete, your backend confirms thesession_tokenserver-side via the state API before granting access.
The on-device result drives UX; your backend confirms the truth via the state API or a signed webhook (see Receiving results).
Props reference
All web SDK config fields are exposed as camelCase props. Only visible,
clientToken, and workflowTemplateId are required.
Core
| Prop | Type | Description |
| -------------------- | -------- | ----------- |
| visible | boolean | Required. Controls mount/visibility of the full-screen Modal. Flip true to start; false to close. |
| clientToken | string | Required. Short-lived token from your backend's /auth/sdk/token. |
| workflowTemplateId | string | Required. Workflow template UUID from the IDto dashboard. |
| env | 'production' \| 'development' | Default 'production'. Selects the CDN bundle + API base + WebView document origin. |
| baseUrl | string | Advanced: override the IDto API base URL (forwarded to the web SDK). The WebView document origin follows it, so the bundle's calls stay same-origin. |
User & session
| Prop | Wire key | Type | Description |
| ----------------- | ------------------- | ---- | ----------- |
| merchantUserId | merchant_user_id | string | Your internal user id (shown in the dashboard). |
| phone | phone | string | Pre-fills the mobile OTP step and de-duplicates sessions. |
| sessionToken | session_token | string | Resume an in-progress session. See Resuming a session. |
| startFresh | start_fresh | boolean | Force a new session even if one exists. |
| preVerified | pre_verified | Record<string, boolean> | Module slugs already verified, to skip those steps. |
| accumulatedData | accumulated_data | Record<string, unknown> | Prior-session data to rehydrate (merchant_injects continuity mode). |
| referenceName | reference_name | string | Reference name (e.g. Aadhaar name) for the silent name_match module. |
Branding & display
| Prop | Type | Description |
| ------------- | ---- | ----------- |
| businessName| string | Shown in the widget header. |
| logo | string | Absolute HTTPS logo URL (loaded inside the WebView header). |
| theme | 'light' \| 'dark' | Default 'light'. |
| language | 'en' \| 'hi' | Default 'en'. |
| displayMode | 'full_screen' \| 'bottom_sheet' | Default 'full_screen'. 'full_screen' is an opaque edge-to-edge Modal. 'bottom_sheet' is presented natively as a bottom-anchored sheet over a dim backdrop, at every screen size. |
| colors | IDtoColors | Color overrides — see below. |
| bottomSheet | IDtoBottomSheet | (0.2.0) { minHeight, maxHeight } — native sheet sizing for bottom_sheet mode. minHeight sets the native sheet height (default '90%'); a number is px, a string passes through (e.g. '60%'). maxHeight is accepted for web parity but is not applied by the native shell (only minHeight is). |
IDtoColors — all optional: background, text, text2, border,
primary, secondary, buttonTextColor_primary, buttonTextColor_secondary.
Module configuration
| Prop | Type | Description |
| ------------------------- | ---- | ----------- |
| aadhaarConfig | IDtoAadhaarConfig | See below. |
| panConfig | IDtoPanConfig | { skipContextScreen } — skip the PAN consent screen (you collect consent yourself). |
| faceMatchConfig | IDtoFaceMatchConfig | See below. |
| faceMatchReferenceImage | string | HTTPS URL or base64 data URI to match the captured face against. Overrides the default Aadhaar-photo reference. |
| nameMatchConfig | IDtoNameMatchConfig | See below. |
IDtoAadhaarConfig
| Field | Default | Description |
| ---------------------- | ------- | ----------- |
| digilockerMaxFailures| 2 | DigiLocker failures (1 or 2) before falling back. |
| okycEnabled | false | Enable Aadhaar OTP (OKYC) fallback. Non-compliant — enable only with IDto approval. When true, a "Try another way" CTA also appears on the DigiLocker waiting screen. |
IDtoFaceMatchConfig
| Field | Default | Description |
| ----------------------- | ------------- | ----------- |
| skipLiveness | true | When true, only the face-comparison API is called; liveness (and livenessFailurePolicy) is skipped. |
| threshold | 70 | Minimum match percentage (0–100) to count as a match. The vendor always returns success, so this threshold — not the vendor boolean — decides pass/fail. 3 below-threshold attempts fail the step terminally. |
| livenessFailurePolicy | 'needs_review' | (0.2.0) What to do when the liveness verdict is unavailable (only applies when skipLiveness: false). 'needs_review' blocks the auto-pass and routes to manual review; 'fail_closed' treats it as a failure; 'fail_open' continues anyway (unsafe — opt-in only). An explicit not-live verdict always blocks. |
IDtoNameMatchConfig — { threshold?, decision_mode?, extra_honorifics?,
aliases?, compare_pairs? }. threshold (0–100) turns each pair into a match
bool + overall passed; omit for scores-only. decision_mode ('all' default
| 'any') controls roll-up. aliases collapses equivalent spellings (e.g.
{ mohammad: 'mohammed' }). compare_pairs scopes which pairs decide passed
('aadhaar_vs_pan' | 'aadhaar_vs_bank' | 'pan_vs_bank'). See
Step results → Name match.
Host-only props
These tune the wrapper / native WebView host and are not sent to the web SDK.
| Prop | Type | Default | Description |
| -------------- | ---------- | ------- | ----------- |
| getToken | () => Promise<string> | — | Async source for a fresh clientToken, called automatically when the bearer expires mid-session. Mint server-side and return the new token; the SDK swaps it in and retries — the user sees nothing. See Token auto-refresh. |
| debug | boolean | false | Forward in-WebView console logs and blocked-navigation notices to your JS console. |
| allowedHosts | string[] | [] | (0.2.0) Extra hostnames the WebView may navigate to, on top of the IDto API host, the CDN bucket, and DigiLocker. |
| readyTimeout | number (ms) | 30000 | (0.2.0) How long to wait for the bundle to signal ready before failing with a network_error. |
IDtoLanding reference
IDtoLanding wraps IDtoVerification with a complete landing screen. It accepts
every IDtoVerification prop (forwarded to the sheet) except the ones it
owns: visible, clientToken, getToken (it manages the token lifecycle),
logo / colors (it has its own, richer branding props), and
onWorkflowComplete / onClose (surfaced as onComplete / onDismiss).
| Prop | Type | Description |
| -------------------- | ---- | ----------- |
| getToken | () => Promise<string> | Required. Mints a fresh short-lived client token from your backend. Drives the CTA and is reused for auto-refresh. |
| workflowTemplateId | string | Required. Workflow template UUID. |
| logo | ImageSourcePropType | Brand mark in the native landing header (e.g. require('./logo.png') or { uri }). |
| logoSize | { width: number; height: number } | Logo box size in dp. Default { width: 92, height: 30 }. |
| logoUrl | string | Logo URL passed to the in-sheet web SDK header (must be WebView-loadable: hosted https or data URI). Distinct from logo. |
| brandColor | string | Primary brand color — drives the CTA, accents, and the sheet palette. Default IDto blue (#0019FF). |
| colors | IDtoColors | Full palette override forwarded to the web SDK. Wins over brandColor. |
| copy | Partial<IDtoLandingCopy> | Override any landing copy; unspecified fields fall back to SDK defaults. |
| onComplete | (data: IDtoWorkflowCompleteData) => void | Fired once when verification completes successfully. |
| onDismiss | () => void | Fired when the sheet closes for any reason (after onComplete / onAbandon). |
The landing defaults to displayMode: 'bottom_sheet' with a '90%' sheet — pass
displayMode / bottomSheet to override.
IDtoLandingCopy (all fields overridable; sensible defaults ship in the SDK):
| Field | Default |
| ----- | ------- |
| heroTitle | "Verify your identity" |
| heroSubtitle | "A quick, secure KYC check — complete it in under a minute." (pass '' to hide) |
| heroNote | "Your details are encrypted and used only for this verification." (pass '' to hide) |
| steps | ["Enter details", "Capture documents", "Instant result"] (pass [] to hide) |
| cta | { idle: "Start verification", loading: "Starting…", open: "In progress…", done: "Verify again" } |
| trust | ["Bank-grade encryption", "Processed securely by IDto"] (pass [] to hide) |
| footer | "Powered by IDto · idto.ai" (pass '' to hide) |
Exported helpers (for building your own landing or matching the palette):
import {
DEFAULT_COPY, // the full default IDtoLandingCopy object
IDTO_BRAND, // '#0019FF'
paletteFromBrand, // (brand, override?) => IDtoColors — full palette from one color
contrastText, // (bgHex) => '#000000' | '#ffffff' — WCAG-ish contrast pick
mergeCopy, // (override?) => IDtoLandingCopy — deep-merge over defaults
} from '@idto/react-native-sdk'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 the screen with a new token.
Pass getToken and the SDK recovers transparently: when the in-WebView bundle
hits a 401, it asks the native host for a fresh token, and the SDK answers by
calling your getToken, then retries the failed request once.
<IDtoVerification
visible={visible}
clientToken={clientToken} // your first mint
workflowTemplateId="uuid-v4"
getToken={async () => {
const r = await fetch('https://your-backend.example/api/idto-token', { method: 'POST' })
return (await r.json()).client_token // mint server-side; never ship client_secret
}}
onClose={() => setVisible(false)}
/>getTokenruns natively (it is never sent into the WebView) and answers the bundle over the bridge — no extra wiring needed.- Refresh is single-flight (one mint for simultaneous failures) and loop-safe (a same/empty token surfaces the
401instead of looping). Only401triggers it —402(payment),403, and410(session expired) do not. - Omit
getTokenand behaviour is unchanged: an expired token surfaces asonErrorwith{ error: 'session_expired' }. - Keep
client_secreton your backend —getTokenshould call your server, which mints the token.
Callbacks & data shapes
| Callback | Payload | When |
| -------------------- | ------- | ---- |
| onStepComplete | IDtoStepCompleteData | After each workflow step. |
| onWorkflowComplete | IDtoWorkflowCompleteData | All steps finished. Fires before the final onStepComplete. Confirm session_token server-side. |
| onError | IDtoErrorData | An error occurred at a step (init failures are fatal; others may recover). |
| onAbandon | IDtoAbandonData | User abandoned mid-flow. |
| onClose | (none) | Widget dismissed / torn down. Fires exactly once. |
Payload field names are snake_case because they describe data the web SDK
emits verbatim. The bridge forwards the web SDK's callback object as-is, so at
runtime the payloads carry the same fields as the web integration — including the
per-step result shapes (Step results), the outcome verdict
(Verification outcome), and credit counters
(Credits). The TypeScript interfaces below are a typed subset;
read richer fields from result / accumulated_data (both typed
Record<string, unknown>), or cast.
interface IDtoStepCompleteData {
step: string // module slug, e.g. 'aadhar_verification'
result: Record<string, unknown> // step-specific verified fields — see Step results
accumulated_data: Record<string, unknown> // all verified data merged so far
session_token: string
credits_deducted: number // credits consumed by this step
balance_remaining: number // credits left after this step
}
interface IDtoWorkflowCompleteData {
all_steps: Record<string, unknown>[] // one entry per completed step, in order
accumulated_data: Record<string, unknown> // all verified data merged across every step
session_token: string // confirm this server-side
// Also present at runtime (verbatim from the web SDK): `outcome` — the verdict.
}
interface IDtoErrorData {
step: string // 'init' for fatal load/open failures
error: string // see the error enumeration below
session_token: string // may be '' on init errors
}
interface IDtoAbandonData {
at_step: string // the step the user was on
reason: string // 'user_closed' | 'timeout' | 'error'
session_token: string
}onError — stable error strings
These SDK-generated values are safe to switch on:
| error value | When it fires |
| --- | --- |
| 'network_error' | The CDN bundle or a request failed (no connectivity, DNS failure). On init: { step: 'init', error: 'network_error' }. |
| 'session_expired' | The clientToken / session expired and no getToken recovered it. |
| 'workflow_not_found' | workflowTemplateId does not exist in this environment. |
| 'step_already_complete' | Backend rejected: the step was already completed in a prior session. |
| 'step_completion_failed' | Backend rejected step completion — log the raw error for the backend message. |
| 'insufficient_credits' | Credit balance reached zero — the flow halts. See Credits. |
| 'timeout' | The backend did not respond in time. |
| 'unknown_error' | Catch-all (e.g. IDtoSDK.open() threw, or the bundle loaded without IDtoSDK). |
Retrying after an error:
'network_error'and'timeout'are generally safe to retry — setvisible=false, wait foronClose, then reopen with a freshclientToken. Do not retry'session_expired'without first fetching a new token. Do not retry'step_already_complete'— it means the step succeeded before; resume withsessionTokeninstead.
When
step === 'init', theerrormay be a raw backend string (e.g. an invalid or expired token message). Do not string-match these — log them and show a generic message.
Step results
Each step's verified data arrives in onStepComplete(data).result, and the same
data is merged into onStepComplete(data).accumulated_data and
onWorkflowComplete(data).accumulated_data (keyed by module slug). These are
typed Record<string, unknown>; the shapes below are what the web SDK emits.
Mobile verification
{ mobile_number: '9876543210', verified: true }Aadhaar verification
{
name: 'RAHUL SHARMA',
dob: '1990-01-15',
gender: 'M',
address: { /* address object */ },
masked_aadhaar: 'XXXX-XXXX-1234',
source: 'okyc' | 'upload', // DigiLocker path uses a reference_key instead
verified: true,
}PAN verification
{ pan_number: 'ABCDE1234F', name: 'RAHUL SHARMA', dob: '1990-01-15', verified: true }Face match
{ match_score: 0.97, matched: true, verified: true }Bank verification
{ account_number: '123456789012', ifsc: 'SBIN0001234', account_holder: 'RAHUL SHARMA', verified: true }Name match
name_match is a silent, server-side module: it scores the person's name across
the sources collected in the workflow (Aadhaar vs PAN vs bank). It is
non-blocking — it never stops the user; you decide what to do with the result.
Read it from accumulated_data.name_match (in onWorkflowComplete or
onStepComplete) — not from onStepComplete's result, which is empty {}
for silent modules:
{
aadhaar_name: 'Nikhil Kumar Gupta',
pan_name: 'GUPTA NIKHIL KUMAR',
bank_name: 'Nikhil Gupta',
matches: {
// `match` appears only when nameMatchConfig.threshold is set. A pair is null
// when a source is missing, or { score: null, reason: 'not_comparable' } when
// a name normalizes to nothing — don't auto-reject on those.
aadhaar_vs_pan: { names: ['Nikhil Kumar Gupta', 'GUPTA NIKHIL KUMAR'], score: 100.0, match: true },
aadhaar_vs_bank: { names: ['Nikhil Kumar Gupta', 'Nikhil Gupta'], score: 80.0, match: false },
pan_vs_bank: { names: ['GUPTA NIKHIL KUMAR', 'Nikhil Gupta'], score: 80.0, match: false },
},
// The block below appears ONLY when nameMatchConfig.threshold is set:
threshold: 80,
decision_mode: 'all',
compare_pairs: ['aadhaar_vs_pan', 'aadhaar_vs_bank'],
passed: false, // null when none of the scoped pairs was comparable
}Without nameMatchConfig (or with no threshold), the per-pair match and the
top-level threshold / decision_mode / compare_pairs / passed fields are
omitted — you get scores only and decide the cutoff yourself. Configure it via the
nameMatchConfig prop and pass referenceName for the
primary reference.
Verification outcome
A result carries a verdict that is separate from whether the user finished all
the steps. The session status says did the flow finish; outcome says what
the verdict was. A session can be completed and still be not_verified.
The outcome is surfaced verbatim on the onWorkflowComplete payload (and on the
state API / webhook). It is one of:
| outcome | Meaning |
| --- | --- |
| verified | All checks passed. |
| needs_review | A soft flag — e.g. a below-threshold name match, or a liveness verdict routed to manual review. Finished, but not a clean pass; queue for manual review. |
| not_verified | A hard check failed. |
outcome is null until status reaches completed. Gate access on
outcome === 'verified', not merely on the session having finished — and always
confirm server-side (Receiving results), since the on-device
callback can be tampered with before it reaches your server.
The
IDtoWorkflowCompleteDataTypeScript interface does not currently declareoutcome, but it is present at runtime. Read it with a cast ((data as any).outcome) or — preferred — rely on the authoritative server-sideoutcomefrom the state API / webhook.
Credits
Each step that performs a backend verification consumes credits, reported per step
in onStepComplete:
onStepComplete={(data) => {
console.log('step:', data.step)
console.log('credits used:', data.credits_deducted) // consumed by this step
console.log('credits left:', data.balance_remaining) // remaining in your account
}}- Credits are in credit units, not currency. Ask your IDto integration manager for the mapping to your billing plan.
credits_deducted/balance_remainingare only inonStepComplete— they are not in theonWorkflowCompletepayload.- If the balance reaches zero, the SDK fires
onErrorwith{ error: 'insufficient_credits' }and halts the flow.
Resuming a session
If a user closes the widget mid-flow, resume from where they left off by passing
the same sessionToken (saved from a previous onClose / onAbandon) back in:
<IDtoVerification
visible={visible}
clientToken={freshToken}
workflowTemplateId="your-workflow-uuid"
sessionToken={savedSessionToken} // resumes from the last incomplete step
onClose={() => setVisible(false)}
/>The SDK skips already-completed steps and lands the user on the next pending step.
Pass startFresh to force a brand-new session even if one exists for the user.
Storage warning: if you persist
sessionToken(e.g. AsyncStorage) to resume across app launches, treat it as a secret — it is a bearer for that session. Prefer server-side session storage keyed to your authenticated user for sensitive flows.
Pre-filling data
If you already know the user's phone number, pass phone to pre-fill the mobile
OTP step (it is also sent to the backend for session deduplication):
<IDtoVerification visible clientToken={token} workflowTemplateId={id} phone="9876543210" … />For workflows in merchant_injects continuity mode (where the server retains
no PII), rehydrate prior-step data via accumulatedData, and skip already-verified
modules via preVerified:
<IDtoVerification
visible
clientToken={token}
workflowTemplateId={id}
preVerified={{ mobile_verification: true }}
accumulatedData={{
aadhar_verification: { name: 'RAHUL SHARMA' },
pan_verification: { full_name: 'RAHUL SHARMA' },
}}
…
/>Contact your IDto integration manager to confirm whether your account uses
merchant_injects mode.
Verification modules
The workflow you reference by workflowTemplateId decides which modules 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. See E-sign. |
| 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. See Step results → Name match. |
Environments
| env | CDN bundle | API base |
| --------------- | ---------------------------------------------------------- | ---------------------- |
| production* | …/idto-sdk-bucket/sdk/prod/idto.js | https://prod.idto.ai |
| development | …/idto-sdk-bucket/sdk/dev/idto.js | https://dev.idto.ai |
* default. env selects the CDN bundle and the API base. The WebView document
origin is the API origin too (a secure https context, required for the camera) —
it follows baseUrl when you set one, so the bundle's API calls stay same-origin.
Receiving results
There are two delivery paths for a verification result. Use both: the on-device callbacks for UX, the webhook for truth.
1. On-device callbacks (UX, not authoritative)
onStepComplete / onWorkflowComplete / onAbandon / onError fire inside the
app as the flow progresses — perfect for driving your UI (spinners, progress,
navigating to a result screen). They are not authoritative: the app can be
backgrounded, lose connectivity, or be tampered with before a callback is acted
on. Never grant access based on a callback alone — confirm server-side.
2. State API (server-to-server poll)
GET https://prod.idto.ai/sdk/v2/session/<session_token>/state
Authorization: Bearer <IDTO_API_SECRET>Response includes status (Session statuses), outcome
(the verdict), and accumulated_data (the full verified
fields). Swap the host for https://dev.idto.ai when using env="development".
404 = not found, 410 = expired, 401/403 = auth error.
3. Signed webhook (authoritative)
When a session finishes, IDto's server POSTs the result directly to your
configured webhook URL — independent of whether the app is still open. This is the
reliable path to trigger downstream actions (loan approval, provisioning). The
webhook URL and signing secret are a per-client setting on IDto's side (no SDK
field) — your integration manager provisions them during onboarding.
Headers: Content-Type: application/json, X-Idto-Signature: sha256=<hex>,
X-Idto-Timestamp: <unix-seconds>, X-Idto-Event-Id: <stable id> (dedupe on this).
Payload:
{
"session_id": "42",
"session_token": "sess_abc123",
"status": "completed",
"outcome": "verified",
"completed_steps": ["mobile_verification", "aadhar_verification", "pan_verification"],
"consents": [{ "consent_type": "kyc", "consent_version": "1.0", "consented_at": "2026-06-15T12:00:00Z" }],
"event_id": "evt_42_completed"
}The webhook carries the verdict and a progress snapshot, not the verified PII.
To pull full verified fields, call the state API with session_token.
Verify the signature against the raw request bytes (timestamp + "." + rawBody),
keyed with your webhook secret, using a constant-time compare:
const crypto = require('crypto')
app.post('/webhooks/idto', express.raw({ type: 'application/json' }), (req, res) => {
const received = (req.headers['x-idto-signature'] || '').replace(/^sha256=/, '')
const timestamp = req.headers['x-idto-timestamp'] || ''
const eventId = req.headers['x-idto-event-id'] || ''
const signed = Buffer.concat([Buffer.from(timestamp + '.'), req.body]) // req.body is a Buffer
// Accept the current OR previous secret during a rotation window.
const ok = [process.env.IDTO_WEBHOOK_SECRET, process.env.IDTO_WEBHOOK_SECRET_PREVIOUS]
.filter(Boolean)
.some((secret) => {
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex')
return expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
})
if (!ok) return res.status(401).end()
if (alreadyProcessed(eventId)) return res.status(200).end() // idempotency
const event = JSON.parse(req.body)
// gate downstream actions on event.outcome === 'verified', then mark eventId processed
res.status(200).end() // respond 2xx within ~10s or IDto retries (exponential backoff)
})Deliveries are at-least-once — record processed event_ids and treat repeats
as no-ops. During a secret rotation, verify against both secrets until all
traffic is on the new one. If your endpoint was down and a delivery dead-lettered,
the state API remains available as a fallback.
Session statuses
The session lifecycle (on the state API and in the webhook status field):
| status | Meaning |
| --- | --- |
| in_progress | The user is actively moving through the steps. |
| completed | All steps are done. The verdict is in outcome. |
| abandoned | A zombie/inactive session reaped by the inactivity sweep. |
| expired | The session passed its hard TTL without completing. Distinct from abandoned — an expiry, not an inactivity reap. |
There is no failed status: a failed check is recorded at the step level and
rolled up into outcome: 'not_verified'.
Security notes
- Never ship
client_secretin the app. Issue the short-livedclientTokenfrom your backend. The bundled examples inline a secret only because they talk to a throwaway dev sandbox (andexamples/is excluded from the npm package). - Navigation is allow-listed. The WebView only navigates to the
idto.aidomain family (API, redirect targets), the CDN bucket, and DigiLocker (includingdigilocker.meripehchaan.gov.in) by default. Add your own hosts withallowedHostsif a custombaseUrlor redirect needs them. - Same-origin, no CSP/CORS config. The WebView runs under the IDto API origin
(following
baseUrlwhen set), so the bundle's API calls need no CORS exception — even with a custombaseUrl— and there is noContent-Security-Policyto maintain (unlike the web integration). - Confirm server-side before acting on a
completedresult, and gate onoutcome === 'verified'(Receiving results).
DigiLocker
The Aadhaar module uses DigiLocker OAuth. The web SDK opens DigiLocker in a
new window (window.open); the RN host keeps that flow inside the
WebView and lets it return to the session — no system-browser hop, so the OAuth
callback resolves in-app. The DigiLocker hosts (including
digilocker.meripehchaan.gov.in) are on the navigation allow-list by default.
DigiLocker can be slow under load; the workflow falls back per
aadhaarConfig.digilockerMaxFailures (and to OKYC / upload if your dashboard
config enables it).
E-sign
If your workflow includes an e-sign step (e_sign), the SDK opens the signing
interface inline inside the WebView. The endpoint used for document upload and
retrieval follows a pattern provided during onboarding — the exact URL varies by
deployment. Contact your IDto integration manager for the correct endpoint.
TypeScript
The package ships its own type declarations (dist/*.d.ts) — you get full
autocomplete and checking with no extra setup. All public types are exported from
the package root:
import type {
IDtoVerificationProps,
IDtoLandingProps,
IDtoLandingCopy,
IDtoEnv,
IDtoColors,
IDtoStepCompleteData,
IDtoWorkflowCompleteData,
IDtoAbandonData,
IDtoErrorData,
IDtoAadhaarConfig,
IDtoFaceMatchConfig,
IDtoPanConfig,
IDtoNameMatchConfig,
IDtoBottomSheet,
} from '@idto/react-native-sdk'Remember the payload interfaces are a typed subset of the verbatim web SDK objects — see the note in Callbacks & data shapes.
Patterns
Full screen (default)
<IDtoVerification
visible={visible}
clientToken={token}
workflowTemplateId={id}
displayMode="full_screen"
onWorkflowComplete={(d) => setVisible(false)}
onClose={() => setVisible(false)}
/>Bottom sheet
<IDtoVerification
visible={visible}
clientToken={token}
workflowTemplateId={id}
displayMode="bottom_sheet"
bottomSheet={{ minHeight: '60%' }}
onWorkflowComplete={(d) => setVisible(false)}
onClose={() => setVisible(false)}
/>Pre-filled phone number
<IDtoVerification
visible={visible}
clientToken={token}
workflowTemplateId={id}
phone={userPhone} // pre-fills mobile OTP step
onWorkflowComplete={(d) => setVisible(false)}
/>Zero-layout landing screen
<IDtoLanding
getToken={mintToken}
workflowTemplateId={id}
brandColor="#0019FF"
onComplete={(d) => {/* confirm server-side */}}
onDismiss={() => navigation.goBack()}
/>Troubleshooting
| Symptom | Cause / fix |
| ------- | ----------- |
| "Load failed" / blank WebView on open | The CDN bundle failed to load. Check device connectivity; onError fires with { step: 'init', error: 'network_error' }. Increase readyTimeout on slow networks. |
| onError session_expired at step: 'init' | clientToken is stale. Fetch a fresh token and reopen — or wire getToken for transparent refresh. |
| onError timeout | Backend didn't respond in time. Retry with a fresh token; check connectivity. |
| onError insufficient_credits | Account credit balance hit zero. Top up; see Credits. |
| onError step_already_complete | The step succeeded in a previous session. Resume with sessionToken instead of restarting. |
| CORS / API errors with a custom baseUrl | The WebView document origin follows baseUrl, so calls are same-origin — no CORS exception needed. Confirm baseUrl is the actual API host the bundle should call. |
| Navigation blocked / step stuck | The target host isn't on the allow-list. Add it via allowedHosts. Turn on debug to log blocked-navigation notices. |
| Camera step won't open / black screen | Simulators/emulators have no camera — use a real device. Confirm the OS permission is declared (Expo plugin or manual Info.plist/AndroidManifest.xml) and react-native-webview >= 13.6.0. |
| DigiLocker slow / times out | DigiLocker load is the bottleneck. The flow falls back per aadhaarConfig.digilockerMaxFailures; advise the user to retry. |
| Report download does nothing | Install the optional react-native-blob-util + react-native-share (and rebuild). Without them the download is a no-op with one console warning. |
| Config changes ignored mid-session | Config is read once per open. Close (visible=false), wait for onClose, then reopen with new props. |
| onClose fires twice / session torn down on reopen | Don't toggle visible off→on within ~1s; wait for onClose before reopening. |
Turn on the debug prop to forward in-WebView console output to your JS
console while diagnosing.
Example apps
examples/ contains two one-tap runnable apps (Expo managed and
bare React Native). Each fetches a clientToken from the IDto dev sandbox,
opens a real DriveX verification session, and streams every callback into an
on-screen event log. See examples/README.md for how to
run them (use a real device for camera steps) and the dev-credentials note.
