Web SDK (React)

The Web SDK is a drop-in React component that renders the full verification UI (ID selection, document scan, and active liveness) and calls the verification API for you. It uses a publishable (pk_) key and detects the environment automatically from the key prefix (pk_test_* → sandbox, pk_live_* → production). For shared concepts (supported countries, branding, results, and errors), see Client SDKs.

Install

shell
pnpm add @myazahq/kyc-sdk-react
shell
yarn add @myazahq/kyc-sdk-react
shell
npm install @myazahq/kyc-sdk-react

Usage

<MyazaKYC /> renders a "Verify Identity" button plus the full modal flow. The trigger is a real <button>: pass children to relabel it, className to restyle it, or any other button attribute (disabled, type, aria-*, …). See Customising the trigger button. Import the bundled stylesheet once, anywhere in your app.

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 a redeploy.

tsx
"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"
      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)}
      onClose={() => console.log("closed")}
    />
  );
}

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 a redeploy.

tsx
"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"
      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

PropTypeDefaultDescription
apiKeystringRequired. Sent as Authorization: Bearer. The environment is derived from the key prefix (pk_test_* → sandbox, pk_live_* → production); an unrecognised prefix throws.
workflowIdstringRun a published workflow (wf_…) built in the dashboard. The SDK fetches its configuration and uses it as the source of truth: workflow config wins over overlapping props. Makes country optional. Requires ≥ 2.2.0 (earlier versions ignore the id). See Configure with a workflow.
country'NG' | 'GH' | 'KE' | 'ZA' | 'CI' | stringRequired unless workflowId is set. Country whose ID types are offered. Any ISO-2 country works: the org's grants are enforced server-side.
livenessMode'gestures' | 'flash' | 'both''gestures'How the liveness step proves presence: randomised gestures, the screen-flash sequence, or both. Usually set by a workflow.
deviceIntelligencebooleantrueCollect device + IP fraud signals (Device Intelligence).
countriesArray<{ country, idTypes? }>Multi-region. List more than one country and the flow opens with a country-select step; the picked country's idTypes win. Usually set by a workflow.
idTypesIdType[]all enabled for orgSubset of ID types to offer; must be valid for country.
userData{ firstName?, lastName?, dateOfBirth?, email? }Pre-fills the user's details; fields provided here aren't asked again. email is never asked for: it is the address your organisation can have the applicant emailed at about a decision.
enableSelfiebooleantrueCapture a selfie during liveness.
enableDocumentCapturebooleantrueEnable the document-scan step for document IDs.
allowDocumentUploadbooleantrueAllow picking a document photo from the device (gallery / drag-and-drop) instead of the camera. false hides every "upload instead" affordance, except on the camera-permission-denied screen, where it stays as an escape hatch.
allowDocumentScanbooleantrueAllow scanning the document with the live camera. false never opens the camera for documents: the applicant uploads a photo of each side instead. Keep at least one of allowDocumentScan and allowDocumentUpload on; with both false the camera stays on. Usually set by a workflow.
enableLivenessbooleantrueRun the liveness challenge step. The server can still disable it per ID type.
voiceGuidanceboolean | { enabled?, language? }trueSpoken liveness instructions (accessibility, TTS output, no microphone). false mutes it; { language: 'fr-FR' } sets the voice language.
showThemeTogglebooleantrueShow a light/dark toggle inside the modal header. Set false to hide it; the flow then stays on appearance.theme and the user can't switch it.
fullScreenbooleanfalseForce the flow to render full screen on every device (desktop drops the centred modal; the expand/collapse control is hidden).
disableClosebooleanfalseHide the close (X) button and block all user dismissal (backdrop click, Escape, mobile swipe-down). The flow can then only be closed programmatically via the useMyazaKYC() hook's close(). The terminal "Submitted" step is non-dismissible regardless.
deviceHandoffbooleantrueOn desktop, show a "continue on your phone" screen (QR code + copyable link) before the flow starts, useful when the computer has no webcam. The user can still continue on the current device; when they finish on their phone, the desktop completes automatically and fires onSubmit. Set false to disable. No effect on mobile/touch devices.
consentStepbooleantrueShow the consent (welcome) screen as the first step. Set false when your own app has already asked for the person's consent, and the flow opens straight on its first real step (the contact codes, the country picker, the ID list, the business form, or a scoped flow's own check). Usually set by a workflow (the Consent step's switch). Switching the screen off does not change what your organisation attests to Myaza.
biometricBiometricFlowConfigsee Biometric re-authenticationThe biometric scopes' flow options: selfieReview, resultDelivery ('both' | 'app' | 'webhook'), doneButton and copy (your own words on the face check screens, see below). Usually set by a workflow (the Presence Intelligence panel's Flow section); ignored off the biometric scopes.
appearanceKYCAppearancebrand defaultsBrand & theme the modal: colours, logo, light/dark. See Branding & theming.
consentKYCConsentContentbuilt-in copyOverride the consent/welcome screen title and description. See Consent screen copy.
successKYCSuccessContentbuilt-in copyOverride the success/submitted screen title and description. See Success screen copy.
metadataRecord<string, string>Forwarded with the verify request (include your requestId).
onStart() => voidCalled when the flow opens.
onStepChange(step: KYCStep) => voidCalled on each step transition.
onSubmit(submission: KYCSubmission) => voidCalled when the server accepts the verification. status is always 'pending'.
onResult(result: KYCResult) => voidCalled once with the verdict on a flow that waits for it in the app (a biometric re-authentication on the default delivery). Never on a timeout. See Biometric re-authentication.
onClose() => voidCalled when the user closes the flow.
onError(error: KYCError) => voidCalled for technical errors only; receives a typed KYCError (a real Error with a code). Verification outcomes never come through here. See Errors.
childrenReactNodeVerify IdentityTrigger button label/content. Defaults to Verify with {companyName} when companyName is set, else Verify Identity.
classNamestringTrigger button classes. Merged via tailwind-merge, so your classes override the built-in styling.
other button attrsButtonHTMLAttributesAny standard <button> attribute (disabled, type, aria-*, style, …) is forwarded. onClick is reserved by the SDK.

Biometric re-authentication

A workflow on the biometric-authentication scope confirms it is really your user, matching a live selfie against the face you enrolled. Three options on the workflow, plus your own words for its screens, shape how the flow behaves, on the web exactly as on the mobile SDKs. All three live on the workflow's Presence Intelligence panel in the builder, under Flow, so they need no code change; a prop-configured mount can pass the same biometric block.

  • Where the verdict lands: In the app and on your webhook (the default) keeps the person on one loading screen from the moment the selfie is taken until the check settles, polling the status endpoint for up to a minute, then shows whether it was them; your app hears the verdict in onResult and your webhook receives it as on every other flow. In the app only is the same wait, but the server sends no webhook for that check at all (verification.started and the terminal events included), so the verdict is your app's alone. Webhook only shows the ordinary submitted screen and leaves the verdict to your webhook. Enrolment workflows record the reference and have no verdict to deliver, so they never wait.
  • Selfie review: whether the captured selfie is shown back with Retake and Continue before it is sent. Off by default on the biometric scopes: a face check is a few seconds long, and a review screen is a stop in the middle of it. Turn it on when you want the person to approve the photo first.
  • Done button: whether the final screen carries a Done button. On by default. Turn it off when your app closes the flow itself from onResult (or from onSubmit on a webhook delivery); the screen then stays until your app dismisses the SDK. A hosted page has no host app, so its redirect or close-this-tab note is unaffected.
  • Screen copy: your own title and description for the screens the person sees, on the workflow's Presence Intelligence panel under Face check screens: the loading screen while the check runs ("Checking it's you") and, on a verdict delivered in the app, the verified screen ("You're verified") and the not-verified screen ("We couldn't confirm it's you"). Leave a field empty to keep the default; {firstName} and {lastName} fill from the userData you pass, as on the welcome and success screens. A not-verified description replaces the reason the server sends. Enrolment offers the loading screen only. In code the same block is biometric.copy (waiting, verified, declined, each with title and description).

When the flow waits, the SDK fires a callback with the verdict:

tsx
<MyazaKYC
  apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  workflowId="wf_AbC123dEf456" // a biometric-authentication workflow
  userId="user_42"              // required: the enrolled user being re-authenticated
  onSubmit={(s) => console.log("submitted", s.verificationId)}
  onResult={(r) => {
    // r.status is the same vocabulary GET /api/kyc/status/:id serves:
    // 'approved' | 'declined' | 'in_review' | 'error' | ...
    if (r.status === "approved") unlockSensitiveAction();
  }}
>
  Confirm it is you
</MyazaKYC>

onSubmit still fires the moment the check is submitted. onResult fires once when the wait settles, never when it times out, and carries the state and reason pair only; scores and other result data stay behind your secret key. Treat it as a courtesy to your UI: the webhook (where the flow sends one) and GET /api/kyc/verifications/:id remain the record, and a decision your app enforces should rest on those, not on a value a client reported. MyazaKYCHosted accepts the same onResult for a minted re-authentication session opened on the hosted page.

Configure with a workflow

See Usage for the mount. The workflow's configuration wins over any overlapping props, so the country, ID types, capture steps, branding, and copy all come from the dashboard. Only runtime data (userId, userData, metadata, and your callbacks) stays in code, because a workflow is a shared template and per-user values would leak between visitors.

A workflow is also the recommended way to turn on the extra capture steps: contact OTP (emailVerification / phoneVerification), proof of address (proofOfAddress), NFC chip (nfc), questionnaire, and business (KYB) verification (subjectType: 'business' + business), and to attach server-side decisioning. These are accepted as SDK props too, but configuring them on the workflow means you compose the checks once and change them by re-publishing, with no redeploy. See Workflows.

Customising the trigger button

<MyazaKYC /> renders a real <button>. In addition to the config props above it accepts standard button attributes (the props type is exported as MyazaKYCProps), so you can treat it like any other button:

tsx
<MyazaKYC
  {...config}
  className="w-full rounded-full bg-black px-6 text-white"
  disabled={!ready}
>
  Start verification
</MyazaKYC>
  • children sets the label (falls back to Verify with {companyName} / Verify Identity).
  • className is merged through tailwind-merge, so your classes win over the defaults.
  • style is merged on top of the SDK's injected theme variables, so theming still applies.
  • onClick is owned by the SDK (it opens the modal) and can't be overridden. For a fully custom trigger element, use the useMyazaKYC() hook below.

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:

tsx
import { KYCProvider, useMyazaKYC } from "@myazahq/kyc-sdk-react";

function Trigger() {
  const { open, close, isOpen, currentStep } = useMyazaKYC({
    apiKey: "pk_test_…",
    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 KYCStep values, reported via onStepChange / currentStep. The core path is consentid-type → (id-input for number-only IDs, or document-capture) → livenesssubmitted. Steps enabled by a workflow slot in automatically: email-verification / phone-verification (right after consent), country-select (multi-region), proof-of-address, questionnaire, and the KYB steps (business-details, business-key-people, business-documents, applicant-role).