Updated: 2026-09-06
The provider's SDK runs in-process: on React Native, @sumsub/react-native-mobilesdk-module presents its own full-screen flow, so there is no WebView, no bridge and no hosted page. This is the best camera and liveness UX available, and it is the mode a real deployment should prefer on mobile.
@blinkbitcoin/kyc-react-native- the native source is its/sumsubentry.- The provider SDK peer:
@sumsub/react-native-mobilesdk-module(≥1.40), pluspod installon iOS. - A backend that mints provider access tokens. This repo's
packages/kyc-servicedoes, but any backend of yours will do - the package only ever calls a callback you supply. The one server call isprovider.createSessionfrom@blinkbitcoin/kyc-node(accessTokenProviderFromEnvselects and checks the provider at boot);createAccessTokenApp/createAccessTokenRouterare that endpoint as a preset, andexamples/access-token-demois it as one mutation on an existing GraphQL API.
npm i @blinkbitcoin/kyc-react-native \
@sumsub/react-native-mobilesdk-module react-native-webview @react-native-community/netinfo
cd ios && bundle exec pod installimport { IdentityVerification } from '@blinkbitcoin/kyc-react-native';
import { createSumsubNativeSource } from '@blinkbitcoin/kyc-react-native/sumsub';
const source = createSumsubNativeSource({
getAccessToken: async () => {
const { accessToken } = await yourApi.startVerification(); // your backend
return accessToken;
},
locale: 'en',
});
<IdentityVerification
source={source}
onComplete={(result) => console.log(result.status, result.applicantId)}
onError={(error) => console.warn(error.code, error.message)}
onCancel={() => navigation.goBack()}
/>;getAccessToken is the whole host seam. It is called once by start() and again, by the SDK itself, whenever the token expires - which is why a native source has no refreshToken and isTokenRefreshable(source) is correctly false.
| Option | Type | Default | Notes |
|---|---|---|---|
getAccessToken |
() => Promise<string> |
- | Required. Also the SDK's expiration handler |
locale |
string |
- | Passed to withLocale only when set |
debug |
boolean |
false |
Routes the SDK's log stream to console with a [kyc-sumsub] prefix |
sdk |
SumsubSdkLike |
the real module | Injection point for tests |
Mode 2 needs only a token, but a backend that runs sessions (Tier B, or a
host on kyc-node with the store) wants one session row per attempt, not one
per token expiry. Because getAccessToken is also the SDK's expiration
handler, wiring it straight to verificationSessionStart would open a new
session every time a token expires - and the webhook, which binds to the
user's newest unbound session, would bind the wrong one. Start once and
refresh after:
import { createSessionTokenProvider } from '@blinkbitcoin/kyc-core/hosted';
import { createSumsubNativeSource } from '@blinkbitcoin/kyc-react-native/sumsub';
const tokens = createSessionTokenProvider({
start: async () => {
const { sessionId, accessToken } = await yourApi.verificationSessionStart({ platform: 'IOS' });
return { sessionId, accessToken };
},
refresh: async sessionId => (await yourApi.verificationSessionRefresh(sessionId)).accessToken,
});
const source = createSumsubNativeSource({ getAccessToken: tokens.getAccessToken });The first call starts the session, every later one refreshes it; concurrent
first calls share one start; a failed start leaves nothing behind, so the
next call starts again. tokens.reset() forgets the session for a deliberate
restart, and tokens.sessionId is the id your status query reads later. The
provider is Apollo-free and knows nothing of GraphQL - the two calls are
whatever your API exposes.
isLaunchable(source) is true, so no WebView is rendered. The component shows its launch-screen while source.launch(session, onEvent) runs and the SDK owns the screen. The resolved IdentityVerificationResult is the terminal complete. A cancel emitted before resolution means the user aborted and the resolved status is advisory (typically incomplete); a rejection is a real failure and becomes the error state.
Known v1 limitations: the SDK screen cannot be dismissed programmatically once launched - the only way out is the user's own action inside it. A second launch() call while one is already running is rejected outright (SDK_UNAVAILABLE, "launch already in progress") rather than racing the first over the same screen; the component itself never issues a concurrent launch, but a host driving the source directly should be aware of it.
The SDK prompts for the camera itself. If you want to preflight - to show your own explainer, or to route a permanently blocked permission to Settings - pass checkPermissions:
<IdentityVerification
source={source}
checkPermissions={async () => {
const result = await request(PERMISSIONS.IOS.CAMERA);
return result === RESULTS.BLOCKED ? 'blocked' : result === RESULTS.GRANTED ? 'granted' : 'denied';
}}
onOpenSettings={openSettings}
/>There is no hard dependency on a permissions library. Omit the prop and the SDK prompts; a denied or blocked answer parks on the permission-screen with Try again (and Open settings when onOpenSettings is supplied) and deliberately does not call onError.
Declare the OS strings yourself: NSCameraUsageDescription, NSMicrophoneUsageDescription, android.permission.CAMERA, android.permission.RECORD_AUDIO.
start() rejects with SDK_UNAVAILABLE ("Identity verification is unavailable in this build."). This is deliberate: the peer is optional so that hosts using only modes 2 and 3 never pull a CocoaPods dependency, and a missing peer fails closed with a coded error instead of a native crash.
The repo ships a UI-free test double for the launch branch, so the whole isLaunchable path is exercisable offline:
import { createFakeLaunchableSource } from '@blinkbitcoin/kyc-core/testing';
const fake = createFakeLaunchableSource({ outcome: 'manual', applicantId: 'demo-applicant' });
// fake.controller.approve() / .decline() / .cancel() / .fail(code, message)outcome accepts 'approved' | 'declined' | 'cancel' | 'error' | 'manual'; 'manual' waits for the controller, which is what makes it drivable from an E2E flow. The React Native demo's KYC_MODE=fake-native does exactly this and puts a fake-sdk-screen overlay with fake-sdk-approve / fake-sdk-decline / fake-sdk-cancel on screen while launch() runs; make e2e-fake-native drives it (manually - an Android emulator with its own KYC_MODE=fake-native Metro, not part of the default CI E2E run).
The real Sumsub sandbox is not in CI. Its manual checklist is sumsub.md.
There is no in-process mode on the web in v1: Sumsub's web SDK is itself an iframe, so the hosted mode already covers the browser. The seam exists - a source implementing MountableSource is handed a <div data-testid="verification-mount"> instead of a url - but no adapter ships. See ../architecture/web.md.