smartcomply-web-sdk
v1.0.75
Published
Drop-in identity verification (KYC) widget for web apps — face liveness, NIN/BVN, document verification
Maintainers
Readme
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:
- Identity verification — Validates the user's BVN/NIN against government databases, or extracts data from uploaded documents
- 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
- 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) 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)
<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:
<!-- 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/[email protected]/dist/smartcomply.browser.js"></script>
@1/@latestre-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.Zpin never moves until you manually change the number in your script tag. See npmjs.com/package/smartcomply-web-sdk for release history.
Alternative: npm
npm install smartcomply-web-sdkUnlike the CDN, npm has no auto-updating option — this is true for every npm package, not specific to ours.
npm installresolves to the latest version at the moment you run it, then locks that exact version inpackage-lock.json; it will not change again on its own. Runnpm update smartcomply-web-sdkperiodically (or before each deploy) to pick up new fixes — this stays within the^1.0.xrange already set in yourpackage.json, and we commit to never shipping a breaking change within1.x, so it's always safe to run. Check 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.
<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:
- Create a secure session with the Adhere backend
- Show a branded welcome screen (your brand name + theme from dashboard)
- Let the user select their country and ID type
- Collect their BVN/NIN number or capture their document photo (front, and back if the document has/needs one)
- 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)
- Record a short clip and submit everything to the backend
- Show a result screen and call your
onCompletecallback
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) | | 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) — 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
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.
{
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
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).
const session = await sdk.createSession();
// session.token — used internally for all subsequent API calls
// session.expires_at — ISO timestampLoad Configuration
Fetch your SDK config (brand name, theme, available ID types per country):
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).
Verify Identity (Data Verification)
For BVN/NIN — validates against the government database:
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:
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:
// 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)
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:
{
"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"
}
}
}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").
Field Notes
verification_id— matches theentryIdyouronCompletecallback received when the user submitted.status— unlikeonComplete'sstatus(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 setstatusto"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 checkbiometrics.face_match.verified(anddocument.is_expired, for document verification) to know whether the person actually passed — never gate access onstatusalone.biometrics.face_match— present for every verification type.attempted: falsemeans face matching was skipped (e.g. a document type with no face, like a CAC certificate or NIN slip) —verified/confidence_percentagearenullin that case, meaning "not applicable," not a failure. Whenattempted: true,verified: falseis 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.genderhere 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. Checkdocument.is_expiredseparately — an expired document can still haveverified: trueon 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.
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
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 whitemidnight_blue— Dark mode with blue accentssunset_gold— Warm gold on creamforest_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
- Adhere account — Sign up at the dashboard
- API Key — Format:
pk_live_...(from dashboard → API Keys) - SDK Config — Create in dashboard with your brand name, theme, verification types, channels, and webhook URL
- Client ID — The UUID shown on your SDK Config
- Funded wallet — Each verification deducts from your balance
- Webhook endpoint — A URL on your server to receive verification results
TypeScript Support
Full TypeScript definitions are included. Key types:
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.
# 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:allTesting
Type-check and build the SDK locally before integrating:
cd smartcomply-web-sdk
npm run typecheck
npm run build:allTo 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
