# SmartComply Web SDK

Drop-in identity verification (KYC) widget for web applications. Embed one line of code and let your users verify their identity through facial liveness detection, government ID checks (BVN/NIN), and international document verification (passport, national ID, driver's license).

## Overview

SmartComply SDK is a **KYC-as-a-service** widget that businesses embed in their web apps to verify end-user identities. It handles the full verification flow — from collecting ID details to live face matching — and delivers results asynchronously via webhook.

### Target Audience

- **Fintechs, banks, and lending platforms** that need to onboard customers securely
- **Any business** required to perform Know Your Customer (KYC) checks before granting account access

### What It Does

The SDK verifies that a person is **real**, **alive**, and **who they claim to be**:

1. **Identity verification** — Validates the user's BVN/NIN against government databases, or extracts data from uploaded documents
2. **Liveness detection** — A short passive camera scan (blink + natural head movement, one continuous window) to prove the person is physically present — not a photo or video replay
3. **Face matching** — Compares the user's live selfie against:
   - The government-returned photo (BVN/NIN flow), or
   - The face on their uploaded document (passport/license flow)

### Two Verification Flows

| Flow | How it works | ID Types |
|------|-------------|----------|
| **Data verification** | User enters an ID number → backend validates against government database → face match against government photo | BVN, NIN, and other data-only channels configured in your dashboard |
| **Document verification** | User captures/uploads a document photo (front, plus back if required or offered) → data is extracted + expiry checked → face match against document photo | Passport, National ID (NIN slip/card), Driver's License, Voter's Card, and other document channels configured in your dashboard |

Exactly which ID types are available depends on what's enabled for your account and country in the Adhere dashboard — fetch `config.channels` (see [Load Configuration](#load-configuration)) rather than hardcoding a list.

### End-to-End Flow

```
┌─────────────────────────────────────────────────────────────────────┐
│  YOUR APP                                                           │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  SmartComply Widget (modal)                                   │  │
│  │                                                               │  │
│  │  Welcome → Country → ID Type → Enter ID / Upload Doc          │  │
│  │      → Face Liveness (camera) → Done                          │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│                    submit    │                                      │
│                              ▼                                      │
│                      Adhere Backend                                 │
│                         │                                           │
│               ┌─────────┼──────────┐                               │
│               ▼         ▼          ▼                                │
│          Face Match   Document Read   Gov DB Check                 │
│        (+ liveness)   (extraction)      (BVN/NIN)                  │
│               └─────────┼──────────┘                               │
│                         ▼                                           │
│                    Webhook POST ──────────► YOUR SERVER             │
│                    (signed with HMAC)       (result handler)        │
└─────────────────────────────────────────────────────────────────────┘
```

## Install

### Recommended: CDN with `@1` (set once, stays current automatically)

```html
<script src="https://cdn.jsdelivr.net/npm/smartcomply-web-sdk@1/dist/smartcomply.browser.js"></script>

<script>
  // SDK available as window.SmartComplySDK
  SmartComplySDK.SmartComplyFlow.open({
    apiKey: "your_api_key",
    clientId: "your_client_id",
    onComplete: (result) => console.log("Done:", result),
  });
</script>
```

`@1` always resolves to the latest `1.x.x` release — bug fixes and new features reach your site automatically the moment we publish them, with **no code change on your end, ever**. We commit to never shipping a breaking change as a `1.x` release; if a breaking change is ever needed, it ships as `2.0.0` and `@1` will keep serving the last safe `1.x` release until you deliberately opt in. This is the same versioning model used by most public JS SDKs (Stripe.js, Google Maps, etc.).

Other CDN options:

```html
<!-- Always the newest release, including any future major version — for
     prototyping, or if you specifically want every change instantly -->
<script src="https://cdn.jsdelivr.net/npm/smartcomply-web-sdk@latest/dist/smartcomply.browser.js"></script>

<!-- Pin to one exact version — only if you need manual control over
     updates. Replace X.Y.Z with the version you've tested against. -->
<script src="https://cdn.jsdelivr.net/npm/smartcomply-web-sdk@X.Y.Z/dist/smartcomply.browser.js"></script>
```

> `@1`/`@latest` re-resolve on **every page load** — that's what makes them self-updating, with no rebuild or redeploy needed on your side. An exact `@X.Y.Z` pin never moves until you manually change the number in your script tag. See [npmjs.com/package/smartcomply-web-sdk](https://www.npmjs.com/package/smartcomply-web-sdk) for release history.

### Alternative: npm

```bash
npm install smartcomply-web-sdk
```

> Unlike the CDN, npm has no auto-updating option — this is true for every npm package, not specific to ours. `npm install` resolves to the latest version **at the moment you run it**, then locks that exact version in `package-lock.json`; it will not change again on its own. Run `npm update smartcomply-web-sdk` periodically (or before each deploy) to pick up new fixes — this stays within the `^1.0.x` range already set in your `package.json`, and we commit to never shipping a breaking change within `1.x`, so it's always safe to run. Check [npmjs.com/package/smartcomply-web-sdk](https://www.npmjs.com/package/smartcomply-web-sdk) for the current version number.

---

## Option 1: Drop-in Widget (Recommended)

The fastest way to add KYC. Opens a complete, branded verification modal — handles everything automatically.

```html
<button id="verify-btn">Verify Identity</button>

<script type="module">
  import { SmartComplyFlow } from "smartcomply-web-sdk";

  document.getElementById("verify-btn").onclick = () => {
    SmartComplyFlow.open({
      apiKey: "pk_live_your_api_key",
      clientId: "your-sdk-config-uuid",
      environment: "production",

      onComplete(result) {
        // Verification submitted! Final result arrives via webhook.
        console.log("Entry ID:", result.entryId);
        console.log("Status:", result.status);  // "processing"
        // Update your UI — tell the user to wait for confirmation
      },

      onError(err) {
        console.error("Verification failed:", err.message);
      },

      onClose() {
        // User closed the modal without completing
      },
    });
  };
</script>
```

That's it. The SDK will:

1. Create a secure session with the Adhere backend
2. Show a branded welcome screen (your brand name + theme from dashboard)
3. Let the user select their country and ID type
4. Collect their BVN/NIN number **or** capture their document photo (front, and back if the document has/needs one)
5. Run a passive face liveness scan (camera stays open for a few seconds — the user blinks and turns their head naturally, no discrete step-by-step prompts)
6. Record a short clip and submit everything to the backend
7. Show a result screen and call your `onComplete` callback

### What the User Sees

| Step | Screen | Description |
|------|--------|-------------|
| 1 | Welcome | Your brand name, description, and what to expect |
| 2 | Country | Select country (auto-skipped if only one country configured) |
| 3 | ID Type | Choose from the channels you configured (e.g., BVN, NIN, Passport) |
| 4a | ID Input | Enter BVN/NIN number → instant backend verification against government DB |
| 4b | Document | Capture/upload the document photo — front required; back is required, optional, or not offered depending on the document type (see [Two-Sided Documents](#two-sided-documents)) |
| 5 | Liveness | Camera opens — a single passive scan window; the user blinks and turns their head naturally, no sequential prompts |
| 6 | Done | "Verification Submitted" — user clicks Done, final result arrives via webhook |

### Two-Sided Documents

Some document types capture a back-side photo as well as the front:

| Document | Back side |
|----------|-----------|
| Passport | Never — front only |
| NIN (slip or card) | **Optional** — offered, with a "Skip" option, since not every NIN document has a usable back |
| Driver's License, Voter's Card, National ID Card | **Required** |

This is driven entirely by the `requires_back_side` flag the backend returns per channel in `config.channels` (see [Load Configuration](#load-configuration)) — the drop-in widget handles it automatically. If you're building your own UI (headless), pass both `document`/`id_file` (front) and `document_back` (back) to `liveness.create()`/`startCheck()` when you have one; `document_back` is always optional at the API level regardless of what the UI requires.

### `SmartComplyFlow.open()` Options

```typescript
SmartComplyFlow.open({
  // Required
  apiKey: string,         // Your API key from the Adhere dashboard
  clientId: string,       // UUID from your SDK Config (created in dashboard)

  // Optional
  environment: "production",              // sandbox is not currently available — use "production"
  timeout: number,                        // Request timeout in ms (default: 30000)

  // Callbacks
  onComplete: (result) => void,   // Verification submitted successfully
  onError: (error) => void,       // Unrecoverable error
  onClose: () => void,            // User closed the modal
});
```

### `onComplete` Result

`onComplete` fires as soon as the user finishes their part (the "Verification Submitted" screen renders) — **not** when verification is actually decided. Backend processing (face match, OCR, government DB check) continues after this fires; the pass/fail outcome only ever arrives via [webhook](#receiving-results-webhook).

```typescript
{
  entryId: 42,                          // Use this to track the verification — matches
                                         // verification_id in the webhook payload
  sessionId: "da7623bd-9158-4b56-...",  // The session token used for this flow
  status: "processing",                 // Always "processing" — this is a submission
                                         // receipt, not a verification verdict
  submittedAt: "2026-04-13T...",        // ISO timestamp
  verificationResult: {                 // Only present for data verification (BVN/NIN) —
                                         // the immediate government-DB lookup result.
                                         // This confirms the ID number matched a real
                                         // record; it says nothing about the face match,
                                         // which is still pending at this point.
    status: "success",
    code: "VERIFICATION_COMPLETE",
    data: { first_name: "Amara", last_name: "Okafor", identity_check_id: 123, ... }
  }
}
```

---

## Option 2: Headless (Custom UI)

For full control over the user interface, use the SDK's API methods directly.

### Initialize

```typescript
import { SmartComply } from "smartcomply-web-sdk";

const sdk = new SmartComply({
  apiKey: "pk_live_your_api_key",
  clientId: "your-sdk-config-uuid",
  environment: "production",
});
```

### Create Session

Every verification flow starts with a session. Sessions last 30 minutes and are single-use (revoked after liveness submission).

```typescript
const session = await sdk.createSession();
// session.token — used internally for all subsequent API calls
// session.expires_at — ISO timestamp
```

### Load Configuration

Fetch your SDK config (brand name, theme, available ID types per country):

```typescript
const config = await sdk.initializeConfig();

console.log(config.brand_name);        // "Your Company Name"
console.log(config.verification_type); // ["data_verification"]

// Channels are grouped by country with field definitions:
console.log(config.channels);
// {
//   "nigeria": [
//     { id: 8, name: "National Identity Number (NIN)", fields: [{ type: "input", label: "Identification Number" }], requires_back_side: "optional" },
//     { id: 2, name: "Bank Verification Number Advanced (BVN)", fields: [{ type: "input", label: "Bank Verification Number" }] },
//     { id: 5, name: "Driver's License", fields: [{ type: "upload", label: "Document" }], requires_back_side: true }
//   ]
// }
```

Use `config.channels` to build your own country/ID type selector. Each channel's `fields` array tells you what inputs to render.

For a document-verification channel, `requires_back_side` tells you whether to also collect a back-side photo: `true` (required), `false`/absent (never), or the string `"optional"` (offer it, but the user can skip — see [Two-Sided Documents](#two-sided-documents)).

### Verify Identity (Data Verification)

For BVN/NIN — validates against the government database:

```typescript
const result = await sdk.onboarding.verify({
  identity_type_id: 8,                                    // Channel ID from config.channels
  fields: { identification_number: "12345678901" },       // Field keys from channel's fields definition
});

if (result.status === "success") {
  console.log(result.data.identity_check_id); // Save this — needed for face matching in liveness
  console.log(result.data.first_name);        // "Amara"
}
```

### Liveness Check (with built-in camera UI)

Mount the SDK's liveness UI into any container element:

```typescript
const container = document.getElementById("liveness-container");

const result = await sdk.liveness.startCheck(container, {
  identifier: "12345678901",          // The ID number entered by the user
  identifier_type: "NIN",
  country: "NG",
  document: documentFrontBlob,        // Optional: document front photo (document flow)
  document_back: documentBackBlob,    // Optional: document back photo, if the channel's
                                       // requires_back_side is true or "optional"
  identity_check: identityCheckId,    // Optional: from verify response (data flow)
}, ["BLINK", "TURN_HEAD"]);

console.log(result.status); // "processing"
```

The camera UI runs a single passive scan (the user blinks and turns their head naturally within one window) rather than stepping through discrete prompts. The `actions` array (3rd argument, defaults to `["BLINK", "TURN_HEAD"]`) is sent to the backend as a descriptive tag — shown on your Adhere dashboard — not a live command sequence the UI enforces. The SDK handles camera access, face detection, video recording, and submission end to end.

### Liveness Check (fully manual)

If you want to handle camera and recording yourself:

```typescript
// 1. Create entry
const entry = await sdk.liveness.create({
  identifier: "A12345678",
  identifier_type: "Passport",
  country: "US",
  challenge_actions: ["BLINK", "TURN_HEAD"],  // Descriptive tag, not a live prompt sequence
  autoshot_file: selfieBlob,          // Captured selfie (JPEG/PNG, max 5MB)
  document: passportFrontBlob,        // Optional: document front photo
  document_back: passportBackBlob,    // Optional: document back photo (rarely needed —
                                       // most document types are front-only; see
                                       // Two-Sided Documents above)
  identity_check: identityCheckId,    // Optional: from verify response
});

// 2. Run your own camera/detection/recording UI
// ...

// 3. Submit the recorded video + snapshot
const result = await sdk.liveness.submit(entry.id, videoBlob, snapshotBlob);
// result.status === "processing"
```

**After submission, the session is revoked.** Each `clientId` can only complete one full verification.

---

## Receiving Results (Webhook)

Verification is processed asynchronously. After the user submits, the backend runs face matching, OCR (for documents), and the government DB check (for BVN/NIN), then delivers one `liveness.completed` webhook to the URL configured in your SDK Config once everything is done.

The payload shape is the same for both verification types — `verification_type` tells you which one it was, and `document` is only present for document verification.

### Webhook Payload — Data Verification (BVN/NIN)

```json
POST https://your-server.com/webhook
Content-Type: application/json
X-Adhere-Signature: sha256=<hmac-sha256-hex>

{
  "event": "liveness.completed",
  "verification_id": 42,
  "verification_type": "data_verification",
  "status": "passed",
  "failure_reason": null,
  "timestamp": "2026-08-08T10:15:00.000Z",
  "subject": {
    "identifier": "12345678901",
    "identifier_type": "National Identity Number (NIN)",
    "country": "nigeria"
  },
  "biometrics": {
    "liveness_verified": true,
    "face_match": {
      "attempted": true,
      "verified": true,
      "confidence_percentage": 70.0
    },
    "selfie_url": "https://.../autoshot.jpg",
    "face_analysis": {
      "gender": "Female",
      "dominant_emotion": "neutral",
      "face_quality": {
        "face_detected": true,
        "face_confidence": 0.98,
        "blur_score": 142.3,
        "is_blurry": false
      }
    }
  },
  "activity": {
    "session_id": "da7623bd-9158-4b56-a9e4-4bccf3c0133f",
    "started_at": "2026-08-08T10:12:00.000Z",
    "submitted_at": "2026-08-08T10:14:30.000Z",
    "completed_at": "2026-08-08T10:15:00.000Z",
    "duration_seconds": 180
  },
  "request_context": {
    "ip": { "address": "102.67.1.66", "city": "Lagos", "country_code": "NG", "...": "..." },
    "device": { "user_agent": "Mozilla/5.0 ...", "type": "desktop", "os": "Windows", "...": "..." }
  },
  "customer_profile": {
    "first_name": "AMARA",
    "last_name": "OKAFOR",
    "other_name": null,
    "date_of_birth": "01-Jan-1997",
    "age": 29,
    "gender": "Female",
    "id_number": "12345678901",
    "serial_number": null,
    "occupation": null,
    "place_of_birth": null,
    "place_of_live": "...",
    "date_of_issue": null,
    "photo_url": null
  }
}
```

`photo_url` is always `null` here — the government-returned photo isn't exposed in the webhook (it's already used server-side for face matching; the `biometrics.selfie_url` field above is the user's own selfie, not the reference photo).

### Webhook Payload — Document Verification (Passport/License/NIN card)

Same top-level shape, plus a `document` block instead of (or alongside) `customer_profile`:

```json
{
  "event": "liveness.completed",
  "verification_id": 43,
  "verification_type": "document_verification",
  "status": "passed",
  "failure_reason": null,
  "timestamp": "2026-08-08T10:20:00.000Z",
  "subject": {
    "identifier": "A12345678",
    "identifier_type": "Passport",
    "country": "usa"
  },
  "biometrics": { "...": "same shape as the data-verification example above" },
  "activity": { "...": "same shape as the data-verification example above" },
  "request_context": { "...": "same shape as the data-verification example above" },
  "document": {
    "status": "verified",
    "document_type": "passport",
    "is_expired": false,
    "first_name": "AMARA",
    "last_name": "OKAFOR",
    "date_of_birth": "1997-01-01",
    "age": 29,
    "gender": "Female",
    "nationality": "NGA",
    "place_of_birth": "LAGOS",
    "document_number": "A12345678",
    "expiry_date": "2030-06-15",
    "issue_date": "2020-06-15",
    "issuing_authority": "...",
    "place_of_issue": null,
    "address": null,
    "district": null,
    "division": null,
    "location": null,
    "sub_location": null,
    "serial_number": null,
    "barcode_number": null,
    "document_url": "https://.../document.jpg",
    "document_back_url": null,
    "face_match": {
      "attempted": true,
      "verified": true,
      "confidence_percentage": 55.0,
      "threshold_percentage": 35.0,
      "reason": null,
      "selfie_url": "https://.../autoshot.jpg",
      "document_face_url": "https://.../document_face.jpg"
    }
  }
}
```

<details>
<summary>Every field on <code>document</code></summary>

Most of these are extracted from the document and will be `null` if the document type doesn't carry that field (e.g. `serial_number`/`barcode_number` mainly apply to newer Kenyan ID cards) or OCR couldn't read it:

`status`, `document_type`, `is_expired`, `first_name`, `last_name`, `date_of_birth`, `age`, `gender`, `nationality`, `place_of_birth`, `document_number`, `expiry_date`, `issue_date`, `issuing_authority`, `place_of_issue`, `address`, `district`, `division`, `location`, `sub_location`, `serial_number`, `barcode_number`, `document_url`, `document_back_url`, `face_match` (`attempted`, `verified`, `confidence_percentage`, `threshold_percentage`, `reason`, `selfie_url`, `document_face_url`).

`face_match.reason` is populated when `verified` is `false` or the match was skipped — a user-facing explanation (e.g. "Face similarity is below the match threshold").

</details>

### Field Notes

- **`verification_id`** — matches the `entryId` your `onComplete` callback received when the user submitted.
- **`status`** — unlike `onComplete`'s `status` (always `"processing"`), this is terminal — but it means **"the check ran to completion,"  not "the person matched."** A face mismatch, low confidence score, or expired document does **not** set `status` to `"failed"` — it's still `"passed"` with the real outcome recorded deeper in the payload. `status: "failed"` is reserved for cases where the check itself couldn't run (service error, no selfie captured, government DB rejection). **Always check `biometrics.face_match.verified` (and `document.is_expired`, for document verification) to know whether the person actually passed — never gate access on `status` alone.**
- **`biometrics.face_match`** — present for every verification type. `attempted: false` means face matching was skipped (e.g. a document type with no face, like a CAC certificate or NIN slip) — `verified`/`confidence_percentage` are `null` in that case, meaning "not applicable," not a failure. When `attempted: true`, `verified: false` is the real "face did not match" signal.
- **`biometrics.face_analysis`** — supplementary selfie diagnostics (emotion, blur/quality), `null`/absent if analysis failed or no selfie was captured. `gender` here is sourced from the verified document/government record, not estimated from the selfie itself. There is no age estimate — age (where shown) is calculated from the document's date of birth, not the photo.
- **`customer_profile`** — only present for data verification, sourced from the government DB response.
- **`document`** — only present for document verification, sourced from OCR + the document-to-selfie face match. Check `document.is_expired` separately — an expired document can still have `verified: true` on the face match.
- **`request_context`** — IP/device metadata captured at session start; useful for fraud signals (e.g. `ip.country_matches_document`).

### Possible Statuses

| Status | Meaning |
|--------|---------|
| `passed` | The check ran to completion — inspect `biometrics.face_match.verified` (and `document.is_expired` for documents) for the real outcome |
| `failed` | The check itself could not complete — service error, no selfie/document captured, or a government DB rejection. See `failure_reason` for a user-safe description |

### Verify Signature

**Always verify the webhook signature** to prevent spoofing. Two details matter here: the header value is prefixed with `sha256=`, and the signature is computed over the **exact compact-JSON bytes** the server sent (no extra whitespace) — so you must use the **raw request body**, not a re-serialized copy of the parsed object, or the HMAC won't match.

```javascript
const crypto = require("crypto");

// IMPORTANT: register this route with a raw-body parser (not express.json()),
// or capture the raw body via a verify callback — re-stringifying req.body
// after JSON.parse produces different bytes and the signature will never match.
app.post(
  "/webhook/smartcomply",
  express.json({
    verify: (req, res, buf) => { req.rawBody = buf; },
  }),
  (req, res) => {
    const header = req.headers["x-adhere-signature"] || "";
    const signature = header.replace(/^sha256=/, "");
    const secret = process.env.WEBHOOK_SECRET.replace(/-/g, "");
    const expected = crypto
      .createHmac("sha256", secret)
      .update(req.rawBody)
      .digest("hex");

    const isValid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));

    if (!isValid) return res.status(401).send("Bad signature");

    const { event, verification_id, status, biometrics, document } = req.body;

    // status === "passed" only means the check ran to completion — it is
    // NOT the same as "the person is verified". Check the real outcome:
    const faceMatched = biometrics?.face_match?.attempted
      ? biometrics.face_match.verified === true
      : true; // not attempted (e.g. NIN slip, CAC) — nothing to fail here
    const documentOk = document ? document.is_expired === false : true;

    if (event === "liveness.completed" && status === "passed" && faceMatched && documentOk) {
      // ✓ User is verified — update your database
      markUserAsVerified(verification_id);
    } else if (event === "liveness.completed") {
      // Check completed but didn't pass verification (face mismatch, expired
      // document) — or status === "failed" (the check itself couldn't run).
      // Either way, do not treat this as a verified user.
      recordVerificationOutcome(verification_id, req.body);
    }

    res.json({ received: true });
  }
);
```

---

## Challenge Actions

The built-in camera UI (`SmartComplyFlow.open()` and `liveness.startCheck()`) runs a **single passive scan** — the user blinks and turns their head naturally within one continuous window, with no discrete step-by-step prompts. There is no server-side per-action scoring; liveness is confirmed by the scan as a whole (blink + motion detected during the window), not by ticking off each action in `challenge_actions` individually.

The `challenge_actions`/`actions` array you pass to `liveness.create()` or `startCheck()` is sent to the backend as a **descriptive tag only** — it's what shows up in the "Challenge Actions" column on your Adhere dashboard for that verification. It does not change what the widget's camera UI actually asks the user to do.

| Action | Used to describe |
|--------|-------------------|
| `BLINK` | The user blinked during the scan |
| `TURN_HEAD` | The user turned their head during the scan |
| `TURN_LEFT` | Legacy — turned head left (older multi-step challenge design) |
| `TURN_RIGHT` | Legacy — turned head right (older multi-step challenge design) |
| `OPEN_MOUTH` | Legacy — opened mouth (older multi-step challenge design) |

Actions must be **UPPERCASE**. Default (and recommended) is `["BLINK", "TURN_HEAD"]` — it's what the widget actually does and what gets tagged accurately.

---

## Error Handling

```typescript
import { SDKError, AuthError, NetworkError } from "smartcomply-web-sdk";

try {
  await sdk.createSession();
} catch (err) {
  if (err instanceof AuthError) {
    // 401: Invalid API key, expired session, or disabled branch
  } else if (err instanceof NetworkError) {
    // No internet, timeout, DNS failure
  } else if (err instanceof SDKError) {
    // Backend error: validation, insufficient balance, etc.
    console.log(err.statusCode);  // HTTP status
    console.log(err.errorCode);   // Machine-readable code
    console.log(err.errorData);   // Additional error details
  }
}
```

| Error Code | HTTP | Meaning |
|------------|------|---------|
| `INVALID_API_KEY` | 401 | Bad/missing `apiKey`, malformed `Authorization` header, or the branch isn't enabled for onboarding — only ever returned at session creation (`createSession()`), not after |
| `INVALID_SESSION` | 401 | Session token missing, malformed, expired (30 min), or already revoked — every call *after* `createSession()` uses this, not `INVALID_API_KEY`. Fix: call `createSession()` again; `apiKey`/`clientId` are unchanged and reused as-is |
| `SDK_CONFIG_NOT_FOUND` | 404 | Invalid `clientId` |
| `VALIDATION_ERROR` | 400 | Missing or invalid fields |
| `RETRY_LIMIT_EXCEEDED` | 429 | User exceeded the retry limit for confirmation/liveness attempts — they must restart with a new session |
| `INSUFFICIENT_BALANCE` | 402 | Top up your wallet |

---

## Theming

The widget automatically uses the theme from your SDK Config (set in the Adhere dashboard):

- `default` — Clean blue on white
- `midnight_blue` — Dark mode with blue accents
- `sunset_gold` — Warm gold on cream
- `forest_emerald` — Green on mint

No client-side theme configuration required.

---

## Browser Support

Requires camera access (`getUserMedia`), video recording (`MediaRecorder`), and WebAssembly.

| Browser | Version |
|---------|---------|
| Chrome | 80+ |
| Firefox | 75+ |
| Safari | 14+ |
| Edge | 80+ |

---

## Prerequisites

1. **Adhere account** — Sign up at the dashboard
2. **API Key** — Format: `pk_live_...` (from dashboard → API Keys)
3. **SDK Config** — Create in dashboard with your brand name, theme, verification types, channels, and webhook URL
4. **Client ID** — The UUID shown on your SDK Config
5. **Funded wallet** — Each verification deducts from your balance
6. **Webhook endpoint** — A URL on your server to receive verification results

---

## TypeScript Support

Full TypeScript definitions are included. Key types:

```typescript
import type {
  FlowOptions,
  FlowResult,
  SDKConfig,
  SDKInitConfig,
  SessionResponse,
  VerifyIdentityResponse,
  LivenessCreateResponse,
  LivenessSubmitResponse,
  ChallengeAction,
  LivenessWebhookPayload,
  ApiResponse,
} from "smartcomply-web-sdk";
```

---

## Quick Start (Test in 2 Minutes)

**For production, always use `npm install`. Clone only for testing/contributing.**

```bash
# Install from npm (production)
npm install smartcomply-web-sdk

# Or clone and build locally
git clone https://github.com/386konsult/smartcomply-web-sdk.git
cd smartcomply-web-sdk
npm install
npm run build:all
```

## Testing

Type-check and build the SDK locally before integrating:

```bash
cd smartcomply-web-sdk
npm run typecheck
npm run build:all
```

To test the built widget in a browser, open `demo/index.html` with a local HTTP server and enter your Adhere API key and SDK client ID. The demo calls the real Adhere backend — sandbox is not currently available, so use production credentials.

- **API Key**: From your Adhere dashboard
- **Client ID**: From your SDK Config in the Adhere dashboard

## License

ISC
