npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@idto/react-native-sdk

v0.2.4

Published

React Native wrapper for the IDto identity verification web SDK (CDN bundle in a WebView).

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

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 clientToken from your client_id / client_secret. The secret must never ship in the app.
  • A sandbox environment for development — set env="development" (points at https://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) and IDtoLanding (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 getUserMedia works inside the WebView with the right permissions.
  • DigiLocker OAuth — both same-window redirects and window.open new-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 sessionToken to continue an interrupted flow.
  • Token auto-refresh — supply getToken and 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 configurable readyTimeout, 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 prebuild

The plugin (app.plugin.js) adds, automatically:

  • iOS — NSCameraUsageDescription and NSMicrophoneUsageDescription.
  • Android — android.permission.CAMERA and android.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 it true to start, and set it false in onWorkflowComplete / onClose / onAbandon to 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 for onClose before reopening (a close handshake may still be in flight for ~1s).

Omitting onError or onAbandon silently swallows failures. Always wire at least a console.error stub 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
  1. Your backend mints a short-lived client_token from /auth/sdk/token using your client_id / client_secret. The secret never leaves the server.
  2. Pass that token as the component's clientToken prop and set visible.
  3. The component streams onStepComplete per step and a terminal onWorkflowComplete / onError / onAbandon, then onClose.
  4. On onWorkflowComplete, your backend confirms the session_token server-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)}
/>
  • getToken runs 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 401 instead of looping). Only 401 triggers it — 402 (payment), 403, and 410 (session expired) do not.
  • Omit getToken and behaviour is unchanged: an expired token surfaces as onError with { error: 'session_expired' }.
  • Keep client_secret on your backend — getToken should 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 — set visible=false, wait for onClose, then reopen with a fresh clientToken. Do not retry 'session_expired' without first fetching a new token. Do not retry 'step_already_complete' — it means the step succeeded before; resume with sessionToken instead.

When step === 'init', the error may 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 IDtoWorkflowCompleteData TypeScript interface does not currently declare outcome, but it is present at runtime. Read it with a cast ((data as any).outcome) or — preferred — rely on the authoritative server-side outcome from 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_remaining are only in onStepComplete — they are not in the onWorkflowComplete payload.
  • If the balance reaches zero, the SDK fires onError with { 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_secret in the app. Issue the short-lived clientToken from your backend. The bundled examples inline a secret only because they talk to a throwaway dev sandbox (and examples/ is excluded from the npm package).
  • Navigation is allow-listed. The WebView only navigates to the idto.ai domain family (API, redirect targets), the CDN bucket, and DigiLocker (including digilocker.meripehchaan.gov.in) by default. Add your own hosts with allowedHosts if a custom baseUrl or redirect needs them.
  • Same-origin, no CSP/CORS config. The WebView runs under the IDto API origin (following baseUrl when set), so the bundle's API calls need no CORS exception — even with a custom baseUrl — and there is no Content-Security-Policy to maintain (unlike the web integration).
  • Confirm server-side before acting on a completed result, and gate on outcome === '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.