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) |
| Flutter | myaza_kyc_sdk_flutter | Flutter / Dart |
Both SDKs mirror each other feature-for-feature and call the same endpoints. The SDK resolves its base URL from the environment value — use a pk_test_ key with staging and a pk_live_ key with production.
Supported countries & ID types
Pass a subset of ID types via idTypes to limit what the user can pick; omit it to offer everything enabled for your organization in that country. Only types valid for the selected country are accepted.
| Country | React idTypes keys | Flutter IdType values |
|---|---|---|
NG Nigeria | bvn, nin, vnin, passport, drivers-license, pvc | bvn, nin, vnin, passport, driversLicense, pvc |
GH Ghana | ghana-card, voters, drivers-license, ssnit, passport | ghanaCard, voters, driversLicense, ssnit, passport |
KE Kenya | national-id, passport | nationalId, passport |
ZA South Africa | national-id | nationalId |
CI Côte d'Ivoire | cni, residence-card | cni, residenceCard |
See ID types for the document requirements (capture vs. number-only, scan sides) of each.
Web SDK (React)
Install
pnpm add @myazahq/kyc-sdk-reactyarn add @myazahq/kyc-sdk-reactnpm install @myazahq/kyc-sdk-reactUsage
<MyazaKYC /> renders a "Verify Identity" button plus the full modal flow. Import the bundled stylesheet once, anywhere in your app.
"use client";
import { MyazaKYC } from "@myazahq/kyc-sdk-react";
import "@myazahq/kyc-sdk-react/styles.css";
export default function VerifyButton() {
return (
<MyazaKYC
apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
environment="staging"
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)}
onClose={() => console.log("closed")}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Required. Sent as Authorization: Bearer. pk_test_* runs in staging, pk_live_* in production. |
environment | 'staging' | 'production' | — | Required. Selects the API base URL — pk_test_* with staging, pk_live_* with production. |
country | 'NG' | 'GH' | 'KE' | 'ZA' | 'CI' | — | Required. Country whose ID types are offered. |
idTypes | IdType[] | all enabled for org | Subset of ID types to offer; must be valid for country. |
userData | { firstName?, lastName?, dateOfBirth? } | — | Pre-fills the user's details; fields provided here aren't asked again. |
enableSelfie | boolean | true | Capture a selfie during liveness. |
enableDocumentCapture | boolean | true | Enable the document-scan step for document IDs. |
enableLiveness | boolean | true | Run the liveness challenge step. The server can still disable it per ID type. |
showThemeToggle | boolean | false | Show a light/dark toggle inside the modal header. |
appearance | KYCAppearance | brand defaults | Brand & theme the modal — colors, logo, light/dark. See Branding & theming. |
consent | KYCConsentContent | built-in copy | Override the consent/welcome screen title and description. See Consent screen copy. |
success | KYCSuccessContent | built-in copy | Override the success/submitted screen title and description. See Success screen copy. |
metadata | Record<string, string> | — | Forwarded with the verify request (include your requestId). |
onStart | () => void | — | Called when the flow opens. |
onStepChange | (step: KYCStep) => void | — | Called on each step transition. |
onSubmit | (submission: KYCSubmission) => void | — | Called when the server accepts the verification. status is always 'pending'. |
onClose | () => void | — | Called when the user closes the flow. |
onError | (error: Error) => void | — | Called for technical errors only (network, 401, 402, upload). Verification outcomes never come through here. |
Programmatic control
For a custom trigger instead of the built-in button, wrap your tree in KYCProvider and drive the flow with the useMyazaKYC() hook:
import { KYCProvider, useMyazaKYC } from "@myazahq/kyc-sdk-react";
function Trigger() {
const { open, close, isOpen, currentStep } = useMyazaKYC({
apiKey: "pk_test_…",
environment: "staging",
country: "NG",
onSubmit: (s) => console.log(s.verificationId),
});
return <button onClick={open} disabled={isOpen}>Verify ({currentStep ?? "idle"})</button>;
}
export default () => (
<KYCProvider>
<Trigger />
</KYCProvider>
);The flow advances through these KYCStep values, reported via onStepChange / currentStep: consent → id-type → id-input → document-capture → liveness → submitted.
Flutter SDK
Install
Add the dependency to your pubspec.yaml, then run flutter pub get:
dependencies:
myaza_kyc_sdk_flutter: ^1.0.0Usage
MyazaKYC.show() opens the full flow as a modal bottom sheet (a full-screen page on Android). Note that context is a named parameter, and the callbacks are passed to show() alongside config — not inside it.
import 'package:flutter/material.dart';
import 'package:myaza_kyc_sdk_flutter/myaza_kyc_sdk_flutter.dart';
void startKYC(BuildContext context) {
MyazaKYC.show(
context: context,
config: MyazaKYCConfig(
apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
environment: KYCEnvironment.staging,
country: Country.NG,
idTypes: const [IdType.bvn, IdType.driversLicense, IdType.passport],
userData: const UserData(firstName: 'Jane', lastName: 'Doe'),
enableSelfie: true,
enableDocumentCapture: true,
enableLiveness: true,
appearance: const MyazaKYCAppearance(
primaryColor: Color(0xFF5645F5),
companyName: 'Myaza',
logo: 'default',
theme: MyazaThemeMode.light,
),
consent: const KYCConsentContent(
title: 'Welcome, {firstName}',
description: "A quick check to confirm it's really you.",
),
success: const KYCSuccessContent(
title: "You're all set, {firstName}!",
description: "We'll email you once your verification is reviewed.",
),
metadata: const {'requestId': 'order_1001', 'userId': 'user_42'},
),
onSubmit: (submission) {
// The verification was created; submission.status is always 'pending'.
// The final result arrives via webhook to your backend (or fetch it with a
// secret-key GET /verifications/:id call — never from the client).
debugPrint('Submitted: ${submission.verificationId}');
},
onError: (error) {
// Technical errors only (network / 401 / 402 / upload).
debugPrint('Error: ${error.code} — ${error.message}');
},
onClose: () => debugPrint('KYC closed'),
);
}Config (MyazaKYCConfig)
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | String | — | Required. Sent as Authorization: Bearer. pk_test_* = staging, pk_live_* = production. |
country | Country | — | Required. Country whose ID types are offered. |
environment | KYCEnvironment | production | Backend the SDK talks to. Base URL is resolved on mount. |
idTypes | List<IdType>? | all for country | Subset of ID types to offer; null shows all for country. |
userData | UserData? | — | Pre-fills the user's details. |
enableSelfie | bool | true | Capture a selfie during liveness. |
enableDocumentCapture | bool | true | Enable the document-scan step for document IDs. |
enableLiveness | bool | true | Run the liveness challenge step. The server can still disable it per ID type. |
appearance | MyazaKYCAppearance? | brand defaults | Brand & theme the flow — colors, logo, light/dark. See Branding & theming. |
consent | KYCConsentContent? | built-in copy | Override the consent/welcome screen title and description. See Consent screen copy. |
success | KYCSuccessContent? | built-in copy | Override the success/submitted screen title and description. See Success screen copy. |
metadata | Map<String, dynamic>? | — | Forwarded with the verify request (include your requestId). |
livenessConfig | LivenessConfig? | 2 challenges, 8s each | Tune the liveness challenge sequence (see below). |
UserData accepts firstName, lastName, dateOfBirth, gender, address, and phoneNumber (all optional).
Callbacks
Passed to MyazaKYC.show() alongside config:
| Callback | Type | Description |
|---|---|---|
onSubmit | void Function(KYCSubmission) | Called when the server accepts the verification. status is always 'pending'. |
onError | void Function(KYCError) | Called for technical errors only (network, 401, 402, upload). Verification outcomes don't come through here. |
onClose | void Function() | Called when the user closes the flow. |
Enums
| Enum | Values |
|---|---|
Country | NG, GH, KE, ZA, CI |
IdType | bvn, nin, vnin, passport, driversLicense, pvc, ghanaCard, voters, ssnit, nationalId, cni, residenceCard |
KYCEnvironment | staging, production |
MyazaThemeMode | light, dark |
Liveness configuration
LivenessConfig tunes the active-liveness step. Defaults match the web SDK.
| Field | Type | Default | Description |
|---|---|---|---|
challengeCount | int | 2 | Number of gesture challenges drawn from the pool. |
challengePool | List<ChallengeConfig>? | kDefaultChallengePool | The set of challenges to draw from. |
timeoutPerChallenge | int | 8 | Seconds allowed per challenge before it fails. |
enableAvatar | bool | true | Show the animated avatar that demonstrates each gesture. |
The default pool covers four LivenessChallenge gestures: nod, turn, blink, and smile.
Branding & theming
Both SDKs accept an appearance object to match the flow to your brand. The same fields exist on each platform — React takes CSS color strings (e.g. "#5645F5"); Flutter takes Color values. On the web, the colors are injected as CSS variables scoped to the SDK (they never leak into your page); setting one color cascades to all of its shades.
| Field | React type | Flutter type | Description |
|---|---|---|---|
primaryColor | string | Color? | Brand color — 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 color. |
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. With the web showThemeToggle, users can flip it. |
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 organization 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, but you can override either through the consent object — identical fields on both SDKs.
| 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, but you can override either through the success object — identical fields on both SDKs.
| 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": "pending", // always 'pending' 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:
| Code | Meaning |
|---|---|
network_error | Connection failure or timeout. |
invalid_api_key | Server returned 401 — check the key and that it matches environment. |
insufficient_credits | Server returned 402. details includes { required, balance, currency }. |
upload_failed | Media upload failed. |
unknown | Anything else. |
In React, onError receives a standard Error; in Flutter it receives a KYCError with code, message, and optional details. See Errors for the API-level error model.
For the full prop reference and styling options, see each package's README.