smartcomply-web-sdk 1.0.70 → 1.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -95
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +18 -3
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/types/liveness.d.ts +21 -2
- package/dist/esm/types/liveness.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +18 -3
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/smartcomply.browser.js +3 -2
- package/dist/smartcomply.browser.js.map +2 -2
- package/dist/types/liveness.d.ts +21 -2
- package/dist/types/liveness.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ SmartComply SDK is a **KYC-as-a-service** widget that businesses embed in their
|
|
|
15
15
|
|
|
16
16
|
The SDK verifies that a person is **real**, **alive**, and **who they claim to be**:
|
|
17
17
|
|
|
18
|
-
1. **Identity verification** — Validates the user's BVN/NIN against government databases, or extracts data from uploaded documents
|
|
19
|
-
2. **Liveness detection** —
|
|
18
|
+
1. **Identity verification** — Validates the user's BVN/NIN against government databases, or extracts data from uploaded documents
|
|
19
|
+
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
|
|
20
20
|
3. **Face matching** — Compares the user's live selfie against:
|
|
21
21
|
- The government-returned photo (BVN/NIN flow), or
|
|
22
22
|
- The face on their uploaded document (passport/license flow)
|
|
@@ -25,8 +25,10 @@ The SDK verifies that a person is **real**, **alive**, and **who they claim to b
|
|
|
25
25
|
|
|
26
26
|
| Flow | How it works | ID Types |
|
|
27
27
|
|------|-------------|----------|
|
|
28
|
-
| **Data verification** | User enters an ID number → backend validates against government database → face match against government photo | BVN, NIN |
|
|
29
|
-
| **Document verification** | User uploads
|
|
28
|
+
| **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 |
|
|
29
|
+
| **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 |
|
|
30
|
+
|
|
31
|
+
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.
|
|
30
32
|
|
|
31
33
|
### End-to-End Flow
|
|
32
34
|
|
|
@@ -46,8 +48,8 @@ The SDK verifies that a person is **real**, **alive**, and **who they claim to b
|
|
|
46
48
|
│ │ │
|
|
47
49
|
│ ┌─────────┼──────────┐ │
|
|
48
50
|
│ ▼ ▼ ▼ │
|
|
49
|
-
│ Face Match
|
|
50
|
-
│
|
|
51
|
+
│ Face Match Document Read Gov DB Check │
|
|
52
|
+
│ (+ liveness) (extraction) (BVN/NIN) │
|
|
51
53
|
│ └─────────┼──────────┘ │
|
|
52
54
|
│ ▼ │
|
|
53
55
|
│ Webhook POST ──────────► YOUR SERVER │
|
|
@@ -64,7 +66,14 @@ npm install smartcomply-web-sdk
|
|
|
64
66
|
### Alternative: CDN (No Build Step)
|
|
65
67
|
|
|
66
68
|
```html
|
|
67
|
-
|
|
69
|
+
<!-- Always latest version -->
|
|
70
|
+
<script src="https://cdn.jsdelivr.net/npm/smartcomply-web-sdk@latest/dist/smartcomply.browser.js"></script>
|
|
71
|
+
|
|
72
|
+
<!-- Or pin to a specific version (recommended for production) — replace X.Y.Z
|
|
73
|
+
with the version you've actually tested against. Current version:
|
|
74
|
+
https://www.npmjs.com/package/smartcomply-web-sdk -->
|
|
75
|
+
<script src="https://cdn.jsdelivr.net/npm/smartcomply-web-sdk@X.Y.Z/dist/smartcomply.browser.js"></script>
|
|
76
|
+
|
|
68
77
|
<script>
|
|
69
78
|
// SDK available as window.SmartComplySDK
|
|
70
79
|
SmartComplySDK.SmartComplyFlow.open({
|
|
@@ -75,6 +84,8 @@ npm install smartcomply-web-sdk
|
|
|
75
84
|
</script>
|
|
76
85
|
```
|
|
77
86
|
|
|
87
|
+
> Pin to a specific version in production to avoid unexpected breaking changes — `@latest` is convenient for prototyping only. See [npmjs.com/package/smartcomply-web-sdk](https://www.npmjs.com/package/smartcomply-web-sdk) for the current version number.
|
|
88
|
+
|
|
78
89
|
---
|
|
79
90
|
|
|
80
91
|
## Option 1: Drop-in Widget (Recommended)
|
|
@@ -117,9 +128,9 @@ That's it. The SDK will:
|
|
|
117
128
|
1. Create a secure session with the Adhere backend
|
|
118
129
|
2. Show a branded welcome screen (your brand name + theme from dashboard)
|
|
119
130
|
3. Let the user select their country and ID type
|
|
120
|
-
4. Collect their BVN/NIN number **or** capture their document photo
|
|
121
|
-
5. Run face liveness
|
|
122
|
-
6. Record a
|
|
131
|
+
4. Collect their BVN/NIN number **or** capture their document photo (front, and back if the document has/needs one)
|
|
132
|
+
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)
|
|
133
|
+
6. Record a short clip and submit everything to the backend
|
|
123
134
|
7. Show a result screen and call your `onComplete` callback
|
|
124
135
|
|
|
125
136
|
### What the User Sees
|
|
@@ -130,9 +141,21 @@ That's it. The SDK will:
|
|
|
130
141
|
| 2 | Country | Select country (auto-skipped if only one country configured) |
|
|
131
142
|
| 3 | ID Type | Choose from the channels you configured (e.g., BVN, NIN, Passport) |
|
|
132
143
|
| 4a | ID Input | Enter BVN/NIN number → instant backend verification against government DB |
|
|
133
|
-
| 4b | Document | Capture
|
|
134
|
-
| 5 | Liveness | Camera opens — user
|
|
135
|
-
| 6 | Done | "Verification Submitted" — user clicks Done, result arrives via webhook |
|
|
144
|
+
| 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)) |
|
|
145
|
+
| 5 | Liveness | Camera opens — a single passive scan window; the user blinks and turns their head naturally, no sequential prompts |
|
|
146
|
+
| 6 | Done | "Verification Submitted" — user clicks Done, final result arrives via webhook |
|
|
147
|
+
|
|
148
|
+
### Two-Sided Documents
|
|
149
|
+
|
|
150
|
+
Some document types capture a back-side photo as well as the front:
|
|
151
|
+
|
|
152
|
+
| Document | Back side |
|
|
153
|
+
|----------|-----------|
|
|
154
|
+
| Passport | Never — front only |
|
|
155
|
+
| NIN (slip or card) | **Optional** — offered, with a "Skip" option, since not every NIN document has a usable back |
|
|
156
|
+
| Driver's License, Voter's Card, National ID Card | **Required** |
|
|
157
|
+
|
|
158
|
+
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.
|
|
136
159
|
|
|
137
160
|
### `SmartComplyFlow.open()` Options
|
|
138
161
|
|
|
@@ -155,12 +178,21 @@ SmartComplyFlow.open({
|
|
|
155
178
|
|
|
156
179
|
### `onComplete` Result
|
|
157
180
|
|
|
181
|
+
`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).
|
|
182
|
+
|
|
158
183
|
```typescript
|
|
159
184
|
{
|
|
160
|
-
entryId: 42,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
entryId: 42, // Use this to track the verification — matches
|
|
186
|
+
// verification_id in the webhook payload
|
|
187
|
+
sessionId: "da7623bd-9158-4b56-...", // The session token used for this flow
|
|
188
|
+
status: "processing", // Always "processing" — this is a submission
|
|
189
|
+
// receipt, not a verification verdict
|
|
190
|
+
submittedAt: "2026-04-13T...", // ISO timestamp
|
|
191
|
+
verificationResult: { // Only present for data verification (BVN/NIN) —
|
|
192
|
+
// the immediate government-DB lookup result.
|
|
193
|
+
// This confirms the ID number matched a real
|
|
194
|
+
// record; it says nothing about the face match,
|
|
195
|
+
// which is still pending at this point.
|
|
164
196
|
status: "success",
|
|
165
197
|
code: "VERIFICATION_COMPLETE",
|
|
166
198
|
data: { first_name: "Amara", last_name: "Okafor", identity_check_id: 123, ... }
|
|
@@ -210,14 +242,17 @@ console.log(config.verification_type); // ["data_verification"]
|
|
|
210
242
|
console.log(config.channels);
|
|
211
243
|
// {
|
|
212
244
|
// "nigeria": [
|
|
213
|
-
// { id: 8, name: "National Identity Number (NIN)", fields: [{ type: "input", label: "Identification Number" }] },
|
|
214
|
-
// { id: 2, name: "Bank Verification Number Advanced (BVN)", fields: [{ type: "input", label: "Bank Verification Number" }] }
|
|
245
|
+
// { id: 8, name: "National Identity Number (NIN)", fields: [{ type: "input", label: "Identification Number" }], requires_back_side: "optional" },
|
|
246
|
+
// { id: 2, name: "Bank Verification Number Advanced (BVN)", fields: [{ type: "input", label: "Bank Verification Number" }] },
|
|
247
|
+
// { id: 5, name: "Driver's License", fields: [{ type: "upload", label: "Document" }], requires_back_side: true }
|
|
215
248
|
// ]
|
|
216
249
|
// }
|
|
217
250
|
```
|
|
218
251
|
|
|
219
252
|
Use `config.channels` to build your own country/ID type selector. Each channel's `fields` array tells you what inputs to render.
|
|
220
253
|
|
|
254
|
+
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)).
|
|
255
|
+
|
|
221
256
|
### Verify Identity (Data Verification)
|
|
222
257
|
|
|
223
258
|
For BVN/NIN — validates against the government database:
|
|
@@ -245,14 +280,16 @@ const result = await sdk.liveness.startCheck(container, {
|
|
|
245
280
|
identifier: "12345678901", // The ID number entered by the user
|
|
246
281
|
identifier_type: "NIN",
|
|
247
282
|
country: "NG",
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
283
|
+
document: documentFrontBlob, // Optional: document front photo (document flow)
|
|
284
|
+
document_back: documentBackBlob, // Optional: document back photo, if the channel's
|
|
285
|
+
// requires_back_side is true or "optional"
|
|
286
|
+
identity_check: identityCheckId, // Optional: from verify response (data flow)
|
|
287
|
+
}, ["BLINK", "TURN_HEAD"]);
|
|
251
288
|
|
|
252
289
|
console.log(result.status); // "processing"
|
|
253
290
|
```
|
|
254
291
|
|
|
255
|
-
The SDK handles camera access, face detection,
|
|
292
|
+
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.
|
|
256
293
|
|
|
257
294
|
### Liveness Check (fully manual)
|
|
258
295
|
|
|
@@ -264,9 +301,12 @@ const entry = await sdk.liveness.create({
|
|
|
264
301
|
identifier: "A12345678",
|
|
265
302
|
identifier_type: "Passport",
|
|
266
303
|
country: "US",
|
|
267
|
-
challenge_actions: ["BLINK", "
|
|
304
|
+
challenge_actions: ["BLINK", "TURN_HEAD"], // Descriptive tag, not a live prompt sequence
|
|
268
305
|
autoshot_file: selfieBlob, // Captured selfie (JPEG/PNG, max 5MB)
|
|
269
|
-
|
|
306
|
+
document: passportFrontBlob, // Optional: document front photo
|
|
307
|
+
document_back: passportBackBlob, // Optional: document back photo (rarely needed —
|
|
308
|
+
// most document types are front-only; see
|
|
309
|
+
// Two-Sided Documents above)
|
|
270
310
|
identity_check: identityCheckId, // Optional: from verify response
|
|
271
311
|
});
|
|
272
312
|
|
|
@@ -284,121 +324,236 @@ const result = await sdk.liveness.submit(entry.id, videoBlob, snapshotBlob);
|
|
|
284
324
|
|
|
285
325
|
## Receiving Results (Webhook)
|
|
286
326
|
|
|
287
|
-
Verification is processed asynchronously. After the user submits, the backend runs face matching
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
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.
|
|
288
330
|
|
|
289
331
|
### Webhook Payload — Data Verification (BVN/NIN)
|
|
290
332
|
|
|
291
333
|
```json
|
|
292
334
|
POST https://your-server.com/webhook
|
|
293
335
|
Content-Type: application/json
|
|
294
|
-
X-Adhere-Signature:
|
|
336
|
+
X-Adhere-Signature: sha256=<hmac-sha256-hex>
|
|
295
337
|
|
|
296
338
|
{
|
|
297
339
|
"event": "liveness.completed",
|
|
298
|
-
"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"
|
|
340
|
+
"verification_id": 42,
|
|
341
|
+
"verification_type": "data_verification",
|
|
342
|
+
"status": "passed",
|
|
343
|
+
"failure_reason": null,
|
|
344
|
+
"timestamp": "2026-08-08T10:15:00.000Z",
|
|
345
|
+
"subject": {
|
|
346
|
+
"identifier": "12345678901",
|
|
347
|
+
"identifier_type": "National Identity Number (NIN)",
|
|
348
|
+
"country": "nigeria"
|
|
349
|
+
},
|
|
350
|
+
"biometrics": {
|
|
351
|
+
"liveness_verified": true,
|
|
352
|
+
"face_match": {
|
|
353
|
+
"attempted": true,
|
|
305
354
|
"verified": true,
|
|
306
|
-
"distance": 0.12,
|
|
307
|
-
"threshold": 0.4,
|
|
308
355
|
"confidence_percentage": 70.0
|
|
309
356
|
},
|
|
310
|
-
"
|
|
357
|
+
"selfie_url": "https://.../autoshot.jpg",
|
|
358
|
+
"face_analysis": {
|
|
359
|
+
"gender": "Female",
|
|
360
|
+
"dominant_emotion": "neutral",
|
|
361
|
+
"face_quality": {
|
|
362
|
+
"face_detected": true,
|
|
363
|
+
"face_confidence": 0.98,
|
|
364
|
+
"blur_score": 142.3,
|
|
365
|
+
"is_blurry": false
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
"activity": {
|
|
370
|
+
"session_id": "da7623bd-9158-4b56-a9e4-4bccf3c0133f",
|
|
371
|
+
"started_at": "2026-08-08T10:12:00.000Z",
|
|
372
|
+
"submitted_at": "2026-08-08T10:14:30.000Z",
|
|
373
|
+
"completed_at": "2026-08-08T10:15:00.000Z",
|
|
374
|
+
"duration_seconds": 180
|
|
375
|
+
},
|
|
376
|
+
"request_context": {
|
|
377
|
+
"ip": { "address": "102.67.1.66", "city": "Lagos", "country_code": "NG", "...": "..." },
|
|
378
|
+
"device": { "user_agent": "Mozilla/5.0 ...", "type": "desktop", "os": "Windows", "...": "..." }
|
|
379
|
+
},
|
|
380
|
+
"customer_profile": {
|
|
381
|
+
"first_name": "AMARA",
|
|
382
|
+
"last_name": "OKAFOR",
|
|
383
|
+
"other_name": null,
|
|
384
|
+
"date_of_birth": "01-Jan-1997",
|
|
385
|
+
"age": 29,
|
|
386
|
+
"gender": "Female",
|
|
387
|
+
"id_number": "12345678901",
|
|
388
|
+
"serial_number": null,
|
|
389
|
+
"occupation": null,
|
|
390
|
+
"place_of_birth": null,
|
|
391
|
+
"place_of_live": "...",
|
|
392
|
+
"date_of_issue": null,
|
|
393
|
+
"photo_url": null
|
|
311
394
|
}
|
|
312
395
|
}
|
|
313
396
|
```
|
|
314
397
|
|
|
315
|
-
|
|
398
|
+
`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).
|
|
399
|
+
|
|
400
|
+
### Webhook Payload — Document Verification (Passport/License/NIN card)
|
|
401
|
+
|
|
402
|
+
Same top-level shape, plus a `document` block instead of (or alongside) `customer_profile`:
|
|
316
403
|
|
|
317
404
|
```json
|
|
318
405
|
{
|
|
319
406
|
"event": "liveness.completed",
|
|
320
|
-
"
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
"
|
|
407
|
+
"verification_id": 43,
|
|
408
|
+
"verification_type": "document_verification",
|
|
409
|
+
"status": "passed",
|
|
410
|
+
"failure_reason": null,
|
|
411
|
+
"timestamp": "2026-08-08T10:20:00.000Z",
|
|
412
|
+
"subject": {
|
|
413
|
+
"identifier": "A12345678",
|
|
414
|
+
"identifier_type": "Passport",
|
|
415
|
+
"country": "usa"
|
|
416
|
+
},
|
|
417
|
+
"biometrics": { "...": "same shape as the data-verification example above" },
|
|
418
|
+
"activity": { "...": "same shape as the data-verification example above" },
|
|
419
|
+
"request_context": { "...": "same shape as the data-verification example above" },
|
|
420
|
+
"document": {
|
|
421
|
+
"status": "verified",
|
|
422
|
+
"document_type": "passport",
|
|
423
|
+
"is_expired": false,
|
|
424
|
+
"first_name": "AMARA",
|
|
425
|
+
"last_name": "OKAFOR",
|
|
426
|
+
"date_of_birth": "1997-01-01",
|
|
427
|
+
"age": 29,
|
|
428
|
+
"gender": "Female",
|
|
429
|
+
"nationality": "NGA",
|
|
430
|
+
"place_of_birth": "LAGOS",
|
|
431
|
+
"document_number": "A12345678",
|
|
432
|
+
"expiry_date": "2030-06-15",
|
|
433
|
+
"issue_date": "2020-06-15",
|
|
434
|
+
"issuing_authority": "...",
|
|
435
|
+
"place_of_issue": null,
|
|
436
|
+
"address": null,
|
|
437
|
+
"district": null,
|
|
438
|
+
"division": null,
|
|
439
|
+
"location": null,
|
|
440
|
+
"sub_location": null,
|
|
441
|
+
"serial_number": null,
|
|
442
|
+
"barcode_number": null,
|
|
443
|
+
"document_url": "https://.../document.jpg",
|
|
444
|
+
"document_back_url": null,
|
|
445
|
+
"face_match": {
|
|
446
|
+
"attempted": true,
|
|
447
|
+
"verified": true,
|
|
448
|
+
"confidence_percentage": 55.0,
|
|
449
|
+
"threshold_percentage": 35.0,
|
|
450
|
+
"reason": null,
|
|
451
|
+
"selfie_url": "https://.../autoshot.jpg",
|
|
452
|
+
"document_face_url": "https://.../document_face.jpg"
|
|
453
|
+
}
|
|
336
454
|
}
|
|
337
455
|
}
|
|
338
456
|
```
|
|
339
457
|
|
|
340
|
-
|
|
458
|
+
<details>
|
|
459
|
+
<summary>Every field on <code>document</code></summary>
|
|
460
|
+
|
|
461
|
+
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:
|
|
462
|
+
|
|
463
|
+
`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`).
|
|
464
|
+
|
|
465
|
+
`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").
|
|
341
466
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
467
|
+
</details>
|
|
468
|
+
|
|
469
|
+
### Field Notes
|
|
470
|
+
|
|
471
|
+
- **`verification_id`** — matches the `entryId` your `onComplete` callback received when the user submitted.
|
|
472
|
+
- **`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.**
|
|
473
|
+
- **`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.
|
|
474
|
+
- **`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.
|
|
475
|
+
- **`customer_profile`** — only present for data verification, sourced from the government DB response.
|
|
476
|
+
- **`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.
|
|
477
|
+
- **`request_context`** — IP/device metadata captured at session start; useful for fraud signals (e.g. `ip.country_matches_document`).
|
|
346
478
|
|
|
347
479
|
### Possible Statuses
|
|
348
480
|
|
|
349
481
|
| Status | Meaning |
|
|
350
482
|
|--------|---------|
|
|
351
|
-
| `passed` |
|
|
352
|
-
| `failed` |
|
|
353
|
-
| `processing` | Still being processed (rare — usually completes in seconds) |
|
|
483
|
+
| `passed` | The check ran to completion — inspect `biometrics.face_match.verified` (and `document.is_expired` for documents) for the real outcome |
|
|
484
|
+
| `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 |
|
|
354
485
|
|
|
355
486
|
### Verify Signature
|
|
356
487
|
|
|
357
|
-
**Always verify the webhook signature** to prevent spoofing:
|
|
488
|
+
**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.
|
|
358
489
|
|
|
359
490
|
```javascript
|
|
360
491
|
const crypto = require("crypto");
|
|
361
492
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
493
|
+
// IMPORTANT: register this route with a raw-body parser (not express.json()),
|
|
494
|
+
// or capture the raw body via a verify callback — re-stringifying req.body
|
|
495
|
+
// after JSON.parse produces different bytes and the signature will never match.
|
|
496
|
+
app.post(
|
|
497
|
+
"/webhook/smartcomply",
|
|
498
|
+
express.json({
|
|
499
|
+
verify: (req, res, buf) => { req.rawBody = buf; },
|
|
500
|
+
}),
|
|
501
|
+
(req, res) => {
|
|
502
|
+
const header = req.headers["x-adhere-signature"] || "";
|
|
503
|
+
const signature = header.replace(/^sha256=/, "");
|
|
504
|
+
const secret = process.env.WEBHOOK_SECRET.replace(/-/g, "");
|
|
505
|
+
const expected = crypto
|
|
506
|
+
.createHmac("sha256", secret)
|
|
507
|
+
.update(req.rawBody)
|
|
508
|
+
.digest("hex");
|
|
509
|
+
|
|
510
|
+
const isValid =
|
|
511
|
+
signature.length === expected.length &&
|
|
512
|
+
crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
|
|
513
|
+
|
|
514
|
+
if (!isValid) return res.status(401).send("Bad signature");
|
|
515
|
+
|
|
516
|
+
const { event, verification_id, status, biometrics, document } = req.body;
|
|
517
|
+
|
|
518
|
+
// status === "passed" only means the check ran to completion — it is
|
|
519
|
+
// NOT the same as "the person is verified". Check the real outcome:
|
|
520
|
+
const faceMatched = biometrics?.face_match?.attempted
|
|
521
|
+
? biometrics.face_match.verified === true
|
|
522
|
+
: true; // not attempted (e.g. NIN slip, CAC) — nothing to fail here
|
|
523
|
+
const documentOk = document ? document.is_expired === false : true;
|
|
524
|
+
|
|
525
|
+
if (event === "liveness.completed" && status === "passed" && faceMatched && documentOk) {
|
|
526
|
+
// ✓ User is verified — update your database
|
|
527
|
+
markUserAsVerified(verification_id);
|
|
528
|
+
} else if (event === "liveness.completed") {
|
|
529
|
+
// Check completed but didn't pass verification (face mismatch, expired
|
|
530
|
+
// document) — or status === "failed" (the check itself couldn't run).
|
|
531
|
+
// Either way, do not treat this as a verified user.
|
|
532
|
+
recordVerificationOutcome(verification_id, req.body);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
res.json({ received: true });
|
|
383
536
|
}
|
|
384
|
-
|
|
385
|
-
res.json({ received: true });
|
|
386
|
-
});
|
|
537
|
+
);
|
|
387
538
|
```
|
|
388
539
|
|
|
389
540
|
---
|
|
390
541
|
|
|
391
542
|
## Challenge Actions
|
|
392
543
|
|
|
393
|
-
|
|
544
|
+
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.
|
|
545
|
+
|
|
546
|
+
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.
|
|
547
|
+
|
|
548
|
+
| Action | Used to describe |
|
|
394
549
|
|--------|-------------------|
|
|
395
|
-
| `BLINK` |
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `OPEN_MOUTH` |
|
|
550
|
+
| `BLINK` | The user blinked during the scan |
|
|
551
|
+
| `TURN_HEAD` | The user turned their head during the scan |
|
|
552
|
+
| `TURN_LEFT` | Legacy — turned head left (older multi-step challenge design) |
|
|
553
|
+
| `TURN_RIGHT` | Legacy — turned head right (older multi-step challenge design) |
|
|
554
|
+
| `OPEN_MOUTH` | Legacy — opened mouth (older multi-step challenge design) |
|
|
400
555
|
|
|
401
|
-
Actions must be **UPPERCASE**.
|
|
556
|
+
Actions must be **UPPERCASE**. Default (and recommended) is `["BLINK", "TURN_HEAD"]` — it's what the widget actually does and what gets tagged accurately.
|
|
402
557
|
|
|
403
558
|
---
|
|
404
559
|
|
|
@@ -425,9 +580,11 @@ try {
|
|
|
425
580
|
|
|
426
581
|
| Error Code | HTTP | Meaning |
|
|
427
582
|
|------------|------|---------|
|
|
428
|
-
| `INVALID_API_KEY` | 401 | Bad
|
|
583
|
+
| `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 |
|
|
584
|
+
| `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 |
|
|
429
585
|
| `SDK_CONFIG_NOT_FOUND` | 404 | Invalid `clientId` |
|
|
430
586
|
| `VALIDATION_ERROR` | 400 | Missing or invalid fields |
|
|
587
|
+
| `RETRY_LIMIT_EXCEEDED` | 429 | User exceeded the retry limit for confirmation/liveness attempts — they must restart with a new session |
|
|
431
588
|
| `INSUFFICIENT_BALANCE` | 402 | Top up your wallet |
|
|
432
589
|
|
|
433
590
|
---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SmartComplyFlow.d.ts","sourceRoot":"","sources":["../../../src/flow/SmartComplyFlow.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"SmartComplyFlow.d.ts","sourceRoot":"","sources":["../../../src/flow/SmartComplyFlow.ts"],"names":[],"mappings":"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"}
|
|
@@ -1660,13 +1660,28 @@ export class SmartComplyFlow {
|
|
|
1660
1660
|
container.appendChild(icon);
|
|
1661
1661
|
const t = document.createElement("h2");
|
|
1662
1662
|
t.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
|
|
1663
|
-
|
|
1663
|
+
// "Photo unclear" only fits the blur/lighting reasons below — a wrong
|
|
1664
|
+
// document entirely (mismatch/no-document-detected) isn't a clarity
|
|
1665
|
+
// problem, so give it its own, accurate title instead of implying the
|
|
1666
|
+
// user just needs better lighting.
|
|
1667
|
+
const isWrongDocument = reason === "document_not_detected"
|
|
1668
|
+
|| reason === "document_type_mismatch"
|
|
1669
|
+
|| reason === "country_mismatch";
|
|
1670
|
+
t.textContent = isWrongDocument ? "That Doesn't Look Right" : "Document Photo Unclear";
|
|
1664
1671
|
container.appendChild(t);
|
|
1665
1672
|
const msg = reason === "image_too_blurry" ? "Your document photo is too blurry. Please retake it in good lighting with a steady hand."
|
|
1666
1673
|
: reason === "image_too_dark" ? "Your document photo is too dark. Please retake it in better lighting."
|
|
1667
1674
|
: reason === "image_too_bright" ? "Your document photo has too much glare. Please reduce reflections and retake."
|
|
1668
|
-
: reason === "document_not_detected" ? "We could not detect a document in the photo. Please
|
|
1669
|
-
:
|
|
1675
|
+
: reason === "document_not_detected" ? "We could not detect a document in the photo. Please make sure you're photographing the actual ID document, with the full document visible."
|
|
1676
|
+
: reason === "document_type_mismatch" ? "This document doesn't match the ID type you selected. Please upload the correct document, or go back and choose the matching type."
|
|
1677
|
+
: reason === "country_mismatch" ? "This document doesn't appear to be from the expected country. Please check you've selected the right country and document."
|
|
1678
|
+
// Any other reason: if it already reads like a real sentence (starts
|
|
1679
|
+
// capitalized, has spaces — matches the backend's own
|
|
1680
|
+
// _safe_failure_reason heuristic for "already user-facing"), show it
|
|
1681
|
+
// as-is instead of masking a specific, accurate reason behind a
|
|
1682
|
+
// generic "bad lighting" message that may not describe what's wrong.
|
|
1683
|
+
: reason && /^[A-Z]/.test(reason) && reason.includes(" ") ? reason
|
|
1684
|
+
: "We could not read your document clearly. Please retake the photo in good lighting with no glare.";
|
|
1670
1685
|
const d = document.createElement("p");
|
|
1671
1686
|
d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
|
|
1672
1687
|
d.textContent = msg;
|