
# SDKs

## Server SDK for Risk Intelligence

Use `@myazahq/trust-sdk` in a Node.js backend for transaction and activity
decisions, customer risk assessment, monitoring, Travel Rule, Investigations and
webhooks. It requires a secret key and never belongs in browser or mobile code.

```bash
npm install @myazahq/trust-sdk
```

```js
import { Myaza } from '@myazahq/trust-sdk';

const myaza = new Myaza({
  apiKey: process.env.MYAZA_SECRET_KEY,
  environment: 'sandbox',
});
```

Follow the [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown) for a
copy-paste transaction decision and the supported customer Entity bootstrap.

## Client SDKs for identity verification

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](https://trust.myaza.co/documentation/api-upload/markdown) it, and [create the verification](https://trust.myaza.co/documentation/api-create-verification/markdown). The SDK uses a **publishable (`pk_`) key**, safe to ship in client code. Results arrive **asynchronously** on your backend via [webhooks](https://trust.myaza.co/documentation/webhooks/markdown), or fetch the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a **secret (`sk_`) key** from your backend (the publishable key can only poll [minimal status](https://trust.myaza.co/documentation/api-verification-status/markdown)).

| 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](#feature-availability).

## Configure with a workflow first

There are two ways to tell an SDK what to run, and they are not equal:

1. **Mount a workflow (the recommended path).** Build the flow in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown), covering countries and ID types, capture steps, add-ons (contact OTP, proof of address, NFC, questionnaire), branding, copy, and server-side [decisioning](https://trust.myaza.co/documentation/decisioning/markdown), 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](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) with zero integration at all.

2. **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)](https://trust.myaza.co/documentation/sdk-react/markdown)**: `<MyazaKYC />` button + modal flow for React (≥ 18).
- **[React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown)**: on-device native liveness; needs a custom dev build (**not Expo Go**).
- **[Flutter SDK](https://trust.myaza.co/documentation/sdk-flutter/markdown)**: `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](https://trust.myaza.co/documentation/sdk-react-native/markdown) 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](https://trust.myaza.co/documentation/workflows/markdown): `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](https://trust.myaza.co/documentation/id-types/markdown)). [Hosted workflow links](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) remain the zero-integration option on any platform.

## Supported countries & ID types

Mounting a [workflow](https://trust.myaza.co/documentation/workflows/markdown)? 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](https://trust.myaza.co/documentation/id-types/markdown) 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](https://trust.myaza.co/documentation/id-types/markdown)): 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](https://trust.myaza.co/documentation/workflows/markdown), 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 from [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown) (`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.

```tsx
// 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](https://trust.myaza.co/documentation/workflows/markdown), 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.

```tsx
// React
consent={{
  title: "Welcome, {firstName}",
  description: "We just need to confirm it's really you. This takes about a minute.",
}}
```

```dart
// 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](https://trust.myaza.co/documentation/workflows/markdown), 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).

```tsx
// React
success={{
  title: "You're all set, {firstName}!",
  description: "We'll email you once your verification is reviewed.",
}}
```

```dart
// 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)).

## Handling results

The SDK finishing only means the verification was **created**, never that it passed. Treat the outcome as eventual:

1. The SDK uploads media and creates the verification, then invokes `onSubmit` with a `KYCSubmission`.
2. Your **backend** receives the terminal [`verification.*` webhook](https://trust.myaza.co/documentation/webhooks/markdown) (or fetches the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a secret key; the SDK's publishable key can only poll [minimal status](https://trust.myaza.co/documentation/api-verification-status/markdown)).
3. You update the user's state from that authoritative result.

The `onSubmit` payload (`KYCSubmission`) carries:

```jsonc
{
  "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 `requestId` in `metadata` so retries are [idempotent](https://trust.myaza.co/documentation/verifications/markdown#idempotency).

## 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 }` for your own logging; the message shown to the end user deliberately names no figure and no cause, since your balance is not something they can act on. Verifications normally pause rather than reaching this. |
| `upload_failed` | Media [upload](https://trust.myaza.co/documentation/api-upload/markdown) 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](https://trust.myaza.co/documentation/errors/markdown) 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; terminal `4xx` surface immediately. The UI shows "Reconnecting… / retrying (n/3)…", and `onError` fires 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.
