Client SDKs
Myaza provides drop-in client SDKs that render the full verification UI (ID selection, document scan, and active liveness) and call the verification API for you. They are thin UI layers: they capture the user's data and media, upload it, and create the verification. The SDK uses a publishable (pk_) key, safe to ship in client code. Results arrive asynchronously on your backend via webhooks, or fetch the full result with a secret (sk_) key from your backend (the publishable key can only poll minimal status).
| SDK | Package | Platform |
|---|---|---|
| Web | @myazahq/kyc-sdk-react | React (≥ 18) |
| React Native | @myazahq/kyc-sdk-react-native | React Native / Expo (SDK ≥ 56) |
| Flutter | myaza_kyc_sdk_flutter | Flutter / Dart |
All three SDKs share the same core flow and call the same endpoints. The SDK detects the environment automatically from the API key prefix and resolves the base URL itself: pk_test_* → sandbox, pk_live_* → production. There is no environment option. Some newer capabilities are web SDK only for now; see Feature availability.
Configure with a workflow first
There are two ways to tell an SDK what to run, and they are not equal:
-
Mount a workflow (the recommended path). Build the flow in the dashboard as a workflow, covering countries and ID types, capture steps, add-ons (contact OTP, proof of address, NFC, questionnaire), branding, copy, and server-side decisioning, then mount it by id:
tsx<MyazaKYC apiKey="pk_live_…" workflowId="wf_AbC123dEf456" userId="user_42" … />The SDK resolves the whole configuration from the server on mount. Changing the flow is a re-publish, not a redeploy, and on mobile not an app-store release. Every published workflow can also be shared as a hosted link with zero integration at all.
-
Configure via props (secondary). Spell the flow out in code (
country,idTypes, step toggles,appearance, copy). Fine for a quick experiment or a single fixed flow, but every change means shipping code, and the add-on steps are far easier composed in the builder.
When both are present, the workflow's configuration wins over any overlapping props. Runtime data (userId, userData, metadata, and your callbacks) always stays in code: a workflow is a shared template and cannot carry per-user values. Everything below that describes a prop also exists as a builder setting on the workflow; treat the props as the code-side equivalents.
Platform guides
Each SDK has its own integration guide. Pick your platform:
- Web SDK (React):
<MyazaKYC />button + modal flow for React (≥ 18). - React Native SDK: on-device native liveness; needs a custom dev build (not Expo Go).
- Flutter SDK:
MyazaKYC.show()modal sheet for Flutter / Dart.
The rest of this page covers what's shared across all three: supported countries, branding, consent/success copy, handling results, errors, and resilience.
The React Native SDK ships native code (on-device liveness via Apple Vision + Google ML Kit), so it needs a custom dev/native build. It does not run in Expo Go. See the React Native SDK guide.
Feature availability
All three SDKs are at full feature parity: the core flow (document capture, selfie, active liveness, consent/success copy, branding, callbacks) and every capability on top of it:
| Capability | Web (2.8.x) | React Native (2.1.x) | Flutter (2.2.x) |
|---|---|---|---|
| Core flow (docs · selfie · liveness · branding · copy) | ✅ | ✅ | ✅ |
Workflows: workflowId embed | ✅ | ✅ | ✅ |
| Capture add-ons: contact OTP, proof of address, questionnaire | ✅ | ✅ | ✅ |
| Business (KYB) verification | ✅ | ✅ | ✅ |
| Global & multi-region countries | ✅ | ✅ | ✅ |
| NFC chip read (eMRTD) | preview only | ✅ | ✅ |
The NFC chip read is an on-device capability: browsers can't talk to a passport chip, so the web SDK renders the chip screen only for the dashboard builder preview, while the native SDKs read chips for real. The read is best-effort by design: a phone without NFC, or a chip that won't read, continues the flow (see ID types). Hosted workflow links remain the zero-integration option on any platform.
Supported countries & ID types
Mounting a workflow? Pick the countries and ID types in the builder, with nothing to pass in code. For prop-configured mounts, pass a subset via idTypes to limit what the user can pick, or omit it to offer everything enabled for your organisation in that country. Only types valid for the selected country are accepted.
All three SDKs use the same kebab-case idTypes keys (Flutter takes them as plain strings; there is no enum):
| Country | idTypes keys (all SDKs) |
|---|---|
NG Nigeria | bvn, nin, vnin, passport, drivers-license, pvc |
GH Ghana | ghana-card, voters, drivers-license, ssnit, passport |
KE Kenya | national-id, passport |
ZA South Africa | national-id |
CI Côte d'Ivoire | cni, residence-card |
See ID types for the document requirements (capture vs. number-only, scan sides) of each.
The five countries above are the government-database-backed markets. All three SDKs additionally verify any ISO country via document capture (Global Documents): pass any two-letter country code, and the ID types your organisation is granted for it are offered automatically (grants are enforced server-side).
Branding & theming
In a workflow, set all of this in the builder's Appearance panel. Every embed and hosted link inherits it, and a re-publish updates it everywhere with no redeploy. The appearance object below is the code-side equivalent for prop-configured mounts (a workflow's values win on overlap).
All three SDKs accept an appearance object to match the flow to your brand. The same fields exist on each platform: React and React Native take CSS-style colour strings (e.g. "#5645F5"); Flutter takes Color values. On the web, the colours are injected as CSS variables scoped to the SDK (they never leak into your page); setting one colour cascades to all of its shades. React Native uses the identical field names and string values.
| Field | React type | Flutter type | Description |
|---|---|---|---|
primaryColor | string | Color? | Brand colour: buttons, selected states, focus rings, the shield hero. |
primaryTextColor | string | Color? | Text/icons rendered on top of primaryColor (e.g. button labels). |
accentColor | string | Color? | Subtle hover/active surfaces. |
backgroundColor | string | Color? | Modal/sheet background. |
surfaceColor | string | Color? | Cards & panels. |
borderColor | string | Color? | Borders and input outlines. |
textColor | string | Color? | Primary text colour. |
companyName | string | String | Shown on the trigger button and the persistent header. |
logo | string | String? | Image URL, or 'default' for your dashboard logo (see below). |
theme | 'light' | 'dark' | MyazaThemeMode | Initial light/dark mode. When showThemeToggle is enabled, users can flip it; otherwise the flow stays on this value. |
Flutter also accepts logoAsset (a bundled Image.asset path) as a fallback when logo is not set.
Logo
The logo renders as a small circular avatar in the header (top-left), persistent on every step, next to companyName.
- An image URL (
"https://…/logo.png") is used directly. 'default'uses the logo configured for your organisation under Settings → Organization in the dashboard. The SDK fetches it fromGET /config(branding.logo) on mount. If your org has no logo set, or the image fails to load, it falls back to the built-in shield icon.- Omitted shows no header logo.
// React — brand with your dashboard logo and a custom palette
appearance={{
primaryColor: "#0F7B6C",
primaryTextColor: "#FFFFFF",
surfaceColor: "#F4F7F6",
logo: "default",
theme: "light",
}}Consent screen copy
The first screen of the flow (the welcome/consent step) shows a heading and a short description. Both default to Myaza's built-in copy. In a workflow, override them in the builder's Consent panel; the consent object below is the code-side equivalent, with identical fields on every SDK.
| Field | React type | Flutter type | Description |
|---|---|---|---|
title | string | String? | Heading. Defaults to Welcome, {firstName} when a first name is known, otherwise Identity Verification. |
description | string | String? | Sub-text under the heading. Defaults to the built-in regulatory copy. |
Both fields support {firstName} and {lastName} tokens, which are replaced with the values you pass in userData (an empty string when absent), so a custom title can still greet the user by name.
// React
consent={{
title: "Welcome, {firstName}",
description: "We just need to confirm it's really you. This takes about a minute.",
}}// Flutter
consent: const KYCConsentContent(
title: 'Welcome, {firstName}',
description: "We just need to confirm it's really you. This takes about a minute.",
),Omit consent (or either field) to keep the defaults.
Success screen copy
The final screen, shown after the user submits, displays a confirmation heading and a short description. Both default to Myaza's built-in copy. In a workflow, override them in the builder's Success panel; the success object below is the code-side equivalent, with identical fields on every SDK.
| Field | React type | Flutter type | Description |
|---|---|---|---|
title | string | String? | Heading. Defaults to Verification Submitted!. |
description | string | String? | Sub-text under the heading. Defaults to the built-in "submitted for review" copy. |
Both fields support the same {firstName} / {lastName} tokens as consent, which are replaced with the values you pass in userData (an empty string when absent).
// React
success={{
title: "You're all set, {firstName}!",
description: "We'll email you once your verification is reviewed.",
}}// Flutter
success: const KYCSuccessContent(
title: "You're all set, {firstName}!",
description: "We'll email you once your verification is reviewed.",
),Omit success (or either field) to keep the defaults. This screen is purely cosmetic; the verification outcome still arrives asynchronously (see Handling results).
Handling results
The SDK finishing only means the verification was created, never that it passed. Treat the outcome as eventual:
- The SDK uploads media and creates the verification, then invokes
onSubmitwith aKYCSubmission. - Your backend receives the terminal
verification.*webhook (or fetches the full result with a secret key; the SDK's publishable key can only poll minimal status). - You update the user's state from that authoritative result.
The onSubmit payload (KYCSubmission) carries:
{
"verificationId": "ver_…", // use this to correlate the webhook / status poll
"status": "processing", // always 'processing' at this point
"metadata": { "requestId": "order_1001" },
"submittedAt": "2026-04-27T12:00:00.000Z"
}Always pass a stable
requestIdinmetadataso retries are idempotent.
Errors
onError fires for technical failures only, never for a failed verification, which arrives asynchronously via webhook. The error carries a stable code (the same set across all three SDKs):
| Code | Meaning |
|---|---|
network_error | Connection failure or timeout, after automatic retries are exhausted. |
invalid_api_key | Server returned 401; check the key (its prefix selects the environment automatically). |
insufficient_credits | Server returned 402. details includes { required, balance, currency }. |
upload_failed | Media upload failed, after automatic retries are exhausted. |
camera_permission_denied | The user denied (or the OS/browser blocks) camera access. |
feature_disabled | Server returned 403; the ID type or a verification feature isn't enabled for your organisation. |
unknown | Anything else. |
All SDKs deliver the same typed error: in React and React Native onError receives a KYCError, a real Error (so existing (error: Error) handlers keep working) that also carries code and optional details; in Flutter it receives a KYCError with code, message, and optional details. See Errors for the API-level error model.
Voice guidance is text-to-speech output; it never records audio, so there is no microphone permission and no microphone error code.
Resilience & capture quality
All three SDKs handle real-world conditions so the flow doesn't crash, hang, or capture unusable media:
-
Network retries. Media uploads and the verify submission retry transient failures (network / timeout /
5xx) with exponential backoff + jitter; terminal4xxsurface immediately. The UI shows "Reconnecting… / retrying (n/3)…", andonErrorfires only after retries are exhausted. -
Camera permission. A denied camera shows a clear "camera access needed" screen (with how to re-enable it: an Open Settings action on Flutter & React Native) and reports
camera_permission_denied. Document capture keeps a gallery-upload fallback on that screen as an escape hatch. -
Multiple faces. During liveness, if more than one face is in frame the challenge pauses ("Make sure only your face is visible") and resumes automatically when only one remains, guarding capture quality and a class of spoofing.
-
Lighting. Too-dark and too-bright (glare) conditions are detected live and surfaced as guidance ("Move to a brighter area" / "Too bright, reduce glare"); auto-capture is discouraged until lighting is acceptable.
-
Liveness is on-device & native on mobile: Apple Vision (iOS) + Google ML Kit (Android) on both React Native and Flutter; the web SDK uses MediaPipe in the browser. The selfie is auto-captured once the challenges pass (you can't trigger it on a static image), and a short liveness video is recorded for server-side review.
For the full prop reference and styling options, see each package's README.