
# 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](https://trust.myaza.co/documentation/sdks/markdown).

## Install

```bash
pnpm add @myazahq/kyc-sdk-react
```

```bash
yarn add @myazahq/kyc-sdk-react
```

```bash
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](#customising-the-trigger-button). Import the bundled stylesheet once, anywhere in your app.

### Recommended: mount a workflow

Build the flow once in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown), 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

| Prop | Type | Default | Description |
|---|---|---|---|
| `apiKey` | `string` | — | **Required.** Sent as `Authorization: Bearer`. The **environment is derived from the key prefix** (`pk_test_*` → sandbox, `pk_live_*` → production); an unrecognised prefix throws. |
| `workflowId` | `string` | — | Run a published [workflow](https://trust.myaza.co/documentation/workflows/markdown) (`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](#configure-with-a-workflow). |
| `country` | `'NG' \| 'GH' \| 'KE' \| 'ZA' \| 'CI' \| string` | — | **Required 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](https://trust.myaza.co/documentation/workflows/markdown). |
| `deviceIntelligence` | `boolean` | `true` | Collect device + IP fraud signals ([Device Intelligence](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons)). |
| `countries` | `Array<{ 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](https://trust.myaza.co/documentation/workflows/markdown). |
| `idTypes` | `IdType[]` | all enabled for org | Subset of [ID types](https://trust.myaza.co/documentation/id-types/markdown) 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](https://trust.myaza.co/documentation/api-review-verification/markdown#emailing-the-applicant). |
| `enableSelfie` | `boolean` | `true` | Capture a selfie during liveness. |
| `enableDocumentCapture` | `boolean` | `true` | Enable the document-scan step for document IDs. |
| `allowDocumentUpload` | `boolean` | `true` | Allow 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. |
| `allowDocumentScan` | `boolean` | `true` | Allow 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](https://trust.myaza.co/documentation/workflows/markdown). |
| `enableLiveness` | `boolean` | `true` | Run the liveness challenge step. The server can still disable it per ID type. |
| `voiceGuidance` | `boolean \| { enabled?, language? }` | `true` | Spoken liveness instructions (accessibility, TTS **output**, no microphone). `false` mutes it; `{ language: 'fr-FR' }` sets the voice language. |
| `showThemeToggle` | `boolean` | `true` | Show 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. |
| `fullScreen` | `boolean` | `false` | Force the flow to render full screen on every device (desktop drops the centred modal; the expand/collapse control is hidden). |
| `disableClose` | `boolean` | `false` | Hide 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()`](#programmatic-control). The terminal "Submitted" step is non-dismissible regardless. |
| `deviceHandoff` | `boolean` | `true` | On **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. |
| `consentStep` | `boolean` | `true` | Show 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](https://trust.myaza.co/documentation/workflows/markdown) (the Consent step's switch). Switching the screen off does not change what your organisation attests to Myaza. |
| `biometric` | `BiometricFlowConfig` | see [Biometric re-authentication](#biometric-re-authentication) | The 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](https://trust.myaza.co/documentation/workflows/markdown) (the Presence Intelligence panel's Flow section); ignored off the biometric scopes. |
| `appearance` | `KYCAppearance` | brand defaults | Brand & theme the modal: colours, logo, light/dark. See [Branding & theming](https://trust.myaza.co/documentation/sdks/markdown#branding-theming). |
| `consent` | `KYCConsentContent` | built-in copy | Override the consent/welcome screen `title` and `description`. See [Consent screen copy](https://trust.myaza.co/documentation/sdks/markdown#consent-screen-copy). |
| `success` | `KYCSuccessContent` | built-in copy | Override the success/submitted screen `title` and `description`. See [Success screen copy](https://trust.myaza.co/documentation/sdks/markdown#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'`. |
| `onResult` | `(result: KYCResult) => void` | — | Called 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](#biometric-re-authentication). |
| `onClose` | `() => void` | — | Called when the user closes the flow. |
| `onError` | `(error: KYCError) => void` | — | Called for **technical** errors only; receives a typed `KYCError` (a real `Error` with a `code`). Verification outcomes never come through here. See [Errors](https://trust.myaza.co/documentation/sdks/markdown#errors). |
| `children` | `ReactNode` | `Verify Identity` | Trigger button label/content. Defaults to `Verify with {companyName}` when `companyName` is set, else `Verify Identity`. |
| `className` | `string` | — | Trigger button classes. Merged via `tailwind-merge`, so your classes override the built-in styling. |
| _other button attrs_ | `ButtonHTMLAttributes` | — | Any 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](#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](https://trust.myaza.co/documentation/decisioning/markdown). 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](https://trust.myaza.co/documentation/workflows/markdown).

## 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](#programmatic-control) 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 `consent` → `id-type` → (`id-input` for number-only IDs, or `document-capture`) → `liveness` → `submitted`. Steps enabled by a [workflow](https://trust.myaza.co/documentation/workflows/markdown) 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`).
