React Native SDK
The React Native SDK mirrors the web SDK's core API (same props, same callbacks) but runs on-device, native liveness: Apple Vision on iOS and Google ML Kit on Android, via a react-native-vision-camera v5 (Nitro) frame processor. Because it ships native code, it needs a custom native build and does not run in Expo Go. For shared concepts (supported countries, branding, results, and errors), see Client SDKs.
Feature availability. This SDK is at full parity with the web and Flutter SDKs: workflow embedding (
workflowId), the capture add-ons (contact OTP, proof of address, questionnaire), business (KYB) verification, any Global Documents country, plus NFC chip reading, which the web SDK can't do at all (browsers can't talk to a passport chip). See Feature availability.
Requirements
| Requirement | Minimum |
|---|---|
| iOS deployment target | 15.1 |
Android minSdkVersion | 24 (Android 7.0) · compileSdk 34 · NDK 27.1 |
| Expo SDK | 56 (React 19, React Native 0.85) |
| React Native | 0.83+, with the New Architecture enabled (VisionCamera v5 / Nitro requires it; Expo SDK 56 enables it by default) |
| Build toolchain | Xcode + CocoaPods (iOS) · JDK 17 for Android Gradle builds |
| Runtime | A dev client or bare build, not Expo Go |
Peer dependencies to install in your app: expo (≥56), react (≥19), react-native (≥0.83), react-native-vision-camera (v5), react-native-vision-camera-worklets (≥5), react-native-worklets (≥0.8), react-native-nitro-modules (≥0.35), react-native-nitro-image (≥0.15), react-native-safe-area-context (≥4), react-native-svg (≥15).
Install
Pick the path that matches your project.
Expo app (managed / prebuild, recommended)
npx expo install @myazahq/kyc-sdk-react-native \
react-native-vision-camera react-native-vision-camera-worklets \
react-native-worklets react-native-nitro-modules react-native-nitro-image \
react-native-safe-area-context react-native-svgAdd the config plugins to app.json (the SDK plugin adds the iOS camera-usage string and Android CAMERA / INTERNET permissions; the VisionCamera plugin wires up the camera + frame processors), then build a dev client:
// app.json
{
"expo": {
"plugins": [
["react-native-vision-camera", { "enableMicrophonePermission": false }],
"@myazahq/kyc-sdk-react-native"
]
}
}npx expo prebuild
npx expo run:ios # or: npx expo run:androidThe SDK plugin accepts an optional custom camera prompt: ["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…" }]. It also adds the location permission strings by default (the Address Intelligence step's "Use my current location" shortcut and attest fix; foreground only, and never required to finish the flow) — pass { "location": false } to opt out if none of your workflows collect an address, or { "locationPermission": "Your message…" } to customise the iOS prompt.
Bare React Native app (no Expo prebuild)
The SDK uses a few expo-* modules, so add the Expo module runtime (you don't need the managed workflow), then install the SDK and its peers:
# 1. One-time: add Expo modules to a bare RN app
npx install-expo-modules@latest
# 2. Install the SDK + peer dependencies
npm install @myazahq/kyc-sdk-react-native \
react-native-vision-camera react-native-vision-camera-worklets \
react-native-worklets react-native-nitro-modules react-native-nitro-image \
react-native-safe-area-context react-native-svg \
expo expo-image-manipulator expo-image-picker expo-speech expo-font \
expo-glass-effect expo-application expo-crypto expo-device expo-localization
# 3. iOS pods
cd ios && pod install && cd ..Then add the native permissions manually (the config plugin only runs under prebuild):
<!-- iOS — ios/<App>/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>We use the camera to photograph your ID and capture a live selfie.</string><!-- Android — android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />If your workflows use the Address Intelligence step, also add the location strings (iOS crashes on the permission request without the usage string):
<!-- iOS — ios/<App>/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location helps place the map pin on your address.</string><!-- Android — android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />The native face-detector module (a VisionCamera v5 Nitro HybridObject plus its Android lib loader) is autolinked, so no manual linking is needed. Ensure the New Architecture is enabled and the worklets/frame-processor build is set up per VisionCamera's setup docs (it relies on react-native-worklets / react-native-vision-camera-worklets, installed above).
Presence reporting (Address Intelligence)
When a workflow enables presence verification, the SDK stores the confirmed pin on-device at capture. Call the reporter from your app on a natural moment (app open works well):
import { reportAddressPresence } from '@myazahq/kyc-sdk-react-native';
const result = await reportAddressPresence({
apiKey: 'pk_live_…',
externalUserId: 'user_42', // the same userId the KYC flow ran with
});
// result.reason: 'reported' | 'no_pin' | 'no_fix' | 'outside_fence' | 'network_error'It never throws and never blocks startup. The geofence is evaluated on-device: only the derived day + night flag is transmitted, never a coordinate. A fix outside the fence sends nothing; a mock-location fix is reported flagged. clearPresencePin(externalUserId) drops the stored pin (sign-out, or after the watch resolves). Stored pins self-expire after 45 days for bounded checks; a pin captured under always-on monitoring never expires until revoked.
Background monitoring (OS geofencing)
The stronger tier: the OS wakes the SDK on fence crossings around the stored pin, app closed or not. Entries stamp a timestamp; exits fold the dwell into per-day aggregates on-device and flush them — the same privacy floor as the foreground tier. Three opt-ins, each deliberate:
- Install the optional peer:
npx expo install expo-task-manager(without it the background tier simply does not exist). - Declare background location via the config plugin —
["@myazahq/kyc-sdk-react-native", { "location": "always" }]. This changes your app's store review posture; the Background Location Declarations page carries the ready-to-paste Play Console and App Review texts. - Register the task at your app's root module (before the component tree), then enable after capture:
// index.js
import { registerBackgroundPresence } from '@myazahq/kyc-sdk-react-native';
registerBackgroundPresence();
// later, once the KYC flow has stored a pin:
const result = await enableBackgroundPresence({ apiKey: 'pk_live_…', externalUserId: 'user_42' });
// result.reason: 'enabled' | 'module_missing' | 'no_pin' | 'foreground_denied' | 'background_denied' | 'start_failed'disableBackgroundPresence() disarms the fence. A refusal at any step leaves the foreground tier working exactly as before: the tiers degrade, never break.
Which tier is running?
Permissions get revoked in Settings and nothing tells the app. Ask, and offer the only honest road back:
import { presenceStatus, openLocationSettings } from '@myazahq/kyc-sdk-react-native';
const status = await presenceStatus('user_42');
// status.tier: 'background' | 'foreground' | 'none' — plus pinStored, alwaysOn,
// both permission states and geofenceArmed
if (status.tier === 'none' && status.pinStored) {
await openLocationSettings(); // no OS allows re-prompting in-app after a denial
}Voice guidance is text-to-speech output. The SDK never records audio, so no microphone permission is requested or required (
enableMicrophonePermission: false).
Usage
<MyazaKYC /> renders a "Verify Identity" trigger plus the full-screen flow. The API is identical to the web SDK, with the same props and callbacks. Unlike the web SDK there is no stylesheet to import: styling is built in.
Recommended: mount a workflow
Build the flow once in the dashboard as a workflow, then mount it by id. The country, ID types, capture steps, branding and copy all come from the workflow, so changing the flow is a re-publish rather than an app release, which matters even more on mobile, where a redeploy means an app-store round trip. Requires ≥ 2.1.0 (the 2.0.x line silently ignores the id).
import { MyazaKYC } from "@myazahq/kyc-sdk-react-native";
export default function VerifyScreen() {
return (
<MyazaKYC
apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
workflowId="wf_AbC123dEf456"
// Runtime data — a workflow is a shared template and cannot carry any of it.
userId="user_42"
userData={{ firstName: "Jane", lastName: "Doe" }}
metadata={{ requestId: "order_1001" }}
onSubmit={(submission) => console.log("submitted", submission.verificationId)}
onError={(err) => console.error(err.code, err.message)}
onClose={() => console.log("closed")}
>
Verify Identity
</MyazaKYC>
);
}userData is worth passing. It is the name you believe the user has, and it is compared against the name read off their document, and that comparison is what produces dataMatch on the verification. Leave it out and the check simply never runs: there is nothing to compare the document against, and dataMatch comes back null.
It cannot live on the workflow. userId, userData and metadata are per-user runtime values, and a workflow is a template shared by every visitor, so these stay in code even when everything else moves to the dashboard.
Or configure everything in code
Skip the workflow and pass the flow's shape as props. Useful for a quick start or a single fixed flow; anything you'd change later means an app release.
import { MyazaKYC } from "@myazahq/kyc-sdk-react-native";
export default function VerifyScreen() {
return (
<MyazaKYC
apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
country="NG"
idTypes={["bvn", "drivers-license", "passport"]}
userData={{ firstName: "Jane", lastName: "Doe" }}
enableSelfie
enableDocumentCapture
enableLiveness
showThemeToggle
appearance={{ primaryColor: "#5645F5", companyName: "Myaza", logo: "default", theme: "light" }}
consent={{ title: "Welcome, {firstName}", description: "A quick check to confirm it's really you." }}
success={{ title: "You're all set, {firstName}!", description: "We'll email you once your verification is reviewed." }}
metadata={{ requestId: "order_1001", userId: "user_42" }}
onSubmit={(submission) => {
// The verification was created; status is always 'pending'.
// Reconcile the final result on your backend via webhook or a secret-key
// GET /verifications/:id call (never from the client).
console.log("submitted", submission.verificationId);
}}
onError={(err) => console.error(err.code, err.message)}
onClose={() => console.log("closed")}
>
Verify Identity
</MyazaKYC>
);
}The SDK accepts the same props as the web SDK's props table, including workflowId, livenessMode, deviceIntelligence, and the capture add-ons (contact OTP, proof of address, questionnaire, NFC). The one exception is className (React Native has no class names); style the trigger by passing style, or render your own trigger with the hook.
disableCloseblocks user dismissal on native too: the iOS swipe-down and the Android back button. Because the built-in<MyazaKYC />trigger has no external close handle, pairdisableClosewith theuseMyazaKYC()hook and call itsclose()to dismiss the flow yourself.
Programmatic control
For a custom trigger, drive the flow with the useMyazaKYC() hook:
import { useMyazaKYC } from "@myazahq/kyc-sdk-react-native";
import { Pressable, Text } from "react-native";
function Trigger() {
const { open, close, isOpen, currentStep } = useMyazaKYC({
apiKey: "pk_test_…",
country: "NG",
onSubmit: (s) => console.log(s.verificationId),
});
return (
<Pressable onPress={open} disabled={isOpen}>
<Text>Verify ({currentStep ?? "idle"})</Text>
</Pressable>
);
}The flow advances through the same KYCStep values as the web SDK: consent → id-type → id-input → document-capture → liveness → submitted.