KyvShield SDK
Integrez la verification d'identite KYC complete dans votre application en quelques minutes.
Integration avec IA
Utilisez Claude Code pour integrer KyvShield automatiquement dans votre projet. Le plugin donne a Claude toute la documentation, les exemples et les types SDK.
Installez le plugin dans Claude Code :
Puis demandez a Claude :
Voir le plugin sur GitHub — Fonctionne avec Claude Code
Flow KYC Complet
Installation
Choisissez votre plateforme
Source: GitHub
getUserMedia) ne fonctionne que sur https:// ou localhost. En production, servez toujours votre page en HTTPS.
Swift Package Manager
NSCameraUsageDescription dans votre Info.plist.
Source: GitHub
Exemple complet
Integration complete par plateforme
https://kyvshield.innolink.sn
https://kyvshield-naruto.innolinkcloud.com
Utilisez l'URL de developpement pour les tests, puis remplacez par l'URL de production pour le deploiement.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:kyvshield_lite/kyvshield_lite.dart';
// ── Possible values ───────────────────────────────────────────────────
// CaptureStep : selfie, recto, verso
// ChallengeMode : minimal, standard, strict
// SelfieDisplayMode : standard, compact, immersive, neonHud
// DocumentDisplayMode: standard, compact, immersive, neonHud
// ChallengeAudioRepeat: once, twice, thrice
// Brightness : light, dark
// ── Fetch available documents from API ────────────────────────────────
final res = await http.get(
Uri.parse('https://kyvshield-naruto.innolinkcloud.com/api/v1/documents'),
headers: {'X-API-Key': 'YOUR_API_KEY'},
);
final docs = (jsonDecode(res.body)['documents'] as List)
.map((d) => KyvshieldDocument.fromJson(d))
.toList();
final selectedDoc = docs.first; // or let user pick
// ── Request camera permission ─────────────────────────────────────────
final granted = await Kyvshield.requestCameraPermission();
if (!granted) {
final denied = await Kyvshield.isCameraPermissionPermanentlyDenied();
if (denied) await Kyvshield.openSettings();
return;
}
// ── Start KYC ─────────────────────────────────────────────────────────
final result = await Kyvshield.initKyc(
context: context,
config: KyvshieldConfig(
baseUrl: 'https://kyvshield-naruto.innolinkcloud.com',
apiKey: 'YOUR_API_KEY',
enableLog: true,
theme: KyvshieldThemeConfig(
primaryColor: Color(0xFFEF8352),
brightness: Brightness.light,
),
),
flow: KyvshieldFlowConfig(
steps: [CaptureStep.selfie, CaptureStep.recto, CaptureStep.verso],
language: 'fr',
showIntroPage: true,
showInstructionPages: true,
showResultPage: true,
showSuccessPerStep: true,
selfieDisplayMode: SelfieDisplayMode.standard,
documentDisplayMode: DocumentDisplayMode.standard,
challengeMode: ChallengeMode.minimal,
requireFaceMatch: true,
playChallengeAudio: true,
maxChallengeAudioPlay: ChallengeAudioRepeat.once,
pauseBetweenAudioPlay: Duration(seconds: 1),
stepChallengeModes: {
CaptureStep.selfie: ChallengeMode.minimal,
CaptureStep.recto: ChallengeMode.standard,
CaptureStep.verso: ChallengeMode.minimal,
},
target: selectedDoc,
kycIdentifier: 'user-12345',
),
);
// ── Handle result ─────────────────────────────────────────────────────
print('Success: ${result.success}');
print('Status: ${result.overallStatus}'); // pass, review, reject, error
print('Session: ${result.sessionId}');
// Selfie
if (result.selfieResult != null) {
print('Live: ${result.selfieResult!.isLive}');
print('Image: ${result.selfieResult!.capturedImage?.length} bytes');
}
// Recto
if (result.rectoResult != null) {
print('Recto score: ${result.rectoResult!.score}');
print('Aligned doc: ${result.rectoResult!.alignedDocument?.length} bytes');
print('Photos: ${result.rectoResult!.extractedPhotos.length}');
print('Fields: ${result.rectoResult!.extraction?.fields.length}');
// Face match (attached to recto)
if (result.rectoResult!.faceVerification != null) {
print('Face match: ${result.rectoResult!.faceVerification!.isMatch}');
print('Similarity: ${result.rectoResult!.faceVerification!.similarityScore}');
}
}
// Verso
if (result.versoResult != null) {
print('Verso score: ${result.versoResult!.score}');
print('Fields: ${result.versoResult!.extraction?.fields.length}');
}
// Extracted data (searches recto + verso)
print('Nom: ${result.getExtractedValue("nom")}');
print('NIN: ${result.getExtractedValue("nin")}');
// Loop all fields sorted by priority
for (final field in result.rectoResult?.extraction?.sortedFields ?? []) {
print('${field.label}: ${field.stringValue}');
}
<!-- Include SDK -->
<script src="https://kyvshield-naruto.innolinkcloud.com/static/sdk/kyvshield.min.js"></script>
<script>
// ── Possible values ───────────────────────────────────────────────────
// steps : 'selfie', 'recto', 'verso'
// challengeMode: 'minimal', 'standard', 'strict'
// selfieDisplayMode / documentDisplayMode: 'standard', 'compact', 'immersive', 'neonHud'
// themeMode : 'light', 'dark'
// ── Fetch documents from API ──────────────────────────────────────────
const res = await fetch('https://kyvshield-naruto.innolinkcloud.com/api/v1/documents', {
headers: { 'X-API-Key': 'YOUR_API_KEY' },
});
const { documents } = await res.json();
const selectedDoc = documents[0]; // or let user pick
// ── Start KYC ─────────────────────────────────────────────────────────
const result = await KyvShield.initKyc(
{
baseUrl: 'https://kyvshield-naruto.innolinkcloud.com',
apiKey: 'YOUR_API_KEY',
enableLog: true,
theme: { primaryColor: '#EF8352', themeMode: 'light' },
},
{
steps: ['selfie', 'recto', 'verso'],
language: 'fr',
showIntroPage: true,
showInstructionPages: true,
showResultPage: true,
showSuccessPerStep: true,
selfieDisplayMode: 'standard',
documentDisplayMode: 'standard',
challengeMode: 'minimal',
requireFaceMatch: true,
playChallengeAudio: true,
maxChallengeAudioPlay: 1,
pauseBetweenAudioPlay: 1000,
target: selectedDoc,
kycIdentifier: 'user-12345',
}
);
// ── Handle result ─────────────────────────────────────────────────────
console.log('Success:', result.success);
console.log('Status:', result.overallStatus); // pass, reject, error
console.log('Session:', result.sessionId);
// Selfie
if (result.selfieResult) {
console.log('Live:', result.selfieResult.isLive);
console.log('Image:', result.selfieResult.capturedImage?.length, 'bytes');
}
// Recto
if (result.rectoResult) {
console.log('Score:', result.rectoResult.score);
console.log('Fields:', result.rectoResult.extraction?.fields?.length);
if (result.rectoResult.faceVerification) {
console.log('Face match:', result.rectoResult.faceVerification.isMatch);
console.log('Similarity:', result.rectoResult.faceVerification.similarityScore);
}
}
// Extracted data
console.log('Nom:', result.getExtractedValue?.('nom'));
console.log('NIN:', result.getExtractedValue?.('nin'));
</script>
import sn.innolink.kyvshield.lite.KyvshieldLite
import sn.innolink.kyvshield.lite.KyvshieldInput
import sn.innolink.kyvshield.lite.config.*
import sn.innolink.kyvshield.lite.result.*
// ── Possible values ───────────────────────────────────────────────────
// CaptureStep : selfie, recto, verso
// ChallengeMode : minimal, standard, strict
// SelfieDisplayMode : standard, compact, immersive, neonHud
// DocumentDisplayMode: standard, compact, immersive, neonHud
// ChallengeAudioRepeat: once, twice, thrice
// ── Fetch documents from API ──────────────────────────────────────────
// GET https://kyvshield-naruto.innolinkcloud.com/api/v1/documents
// Header: X-API-Key: YOUR_API_KEY
// Parse response -> List<KyvshieldDocument> via KyvshieldDocument.fromJson()
// ── With Jetpack Compose (ActivityResultContract) ─────────────────────
val kycLauncher = rememberLauncherForActivityResult(
KyvshieldLite.getResultContract()
) { result: KYCResult ->
// Handle result
println("Success: ${result.success}")
println("Status: ${result.overallStatus}")
}
// Build config
val config = KyvshieldConfig(
baseUrl = "https://kyvshield-naruto.innolinkcloud.com",
apiKey = "YOUR_API_KEY",
enableLog = true,
theme = KyvshieldThemeConfig(
primaryColor = 0xFFEF8352.toInt(),
darkMode = false,
),
)
// Build flow
val flow = KyvshieldFlowConfig(
steps = listOf(CaptureStep.selfie, CaptureStep.recto, CaptureStep.verso),
language = "fr",
showIntroPage = true,
showInstructionPages = true,
showResultPage = true,
showSuccessPerStep = true,
selfieDisplayMode = SelfieDisplayMode.standard,
documentDisplayMode = DocumentDisplayMode.standard,
challengeMode = ChallengeMode.minimal,
requireFaceMatch = true,
playChallengeAudio = true,
maxChallengeAudioPlay = ChallengeAudioRepeat.once,
pauseBetweenAudioPlayMs = 1000L,
stepChallengeModes = mapOf(
CaptureStep.selfie to ChallengeMode.minimal,
CaptureStep.recto to ChallengeMode.standard,
CaptureStep.verso to ChallengeMode.minimal,
),
target = selectedDoc,
kycIdentifier = "user-12345",
)
// Launch KYC
kycLauncher.launch(KyvshieldInput(config, flow))
// ── Handle result ─────────────────────────────────────────────────────
println("Success: ${result.success}")
println("Status: ${result.overallStatus}") // pass, reject, error
println("Session: ${result.sessionId}")
// Selfie
result.selfieResult?.let {
println("Live: ${it.isLive}")
println("Image: ${it.capturedImage?.size ?: 0} bytes")
}
// Recto
result.rectoResult?.let {
println("Recto score: ${it.score}")
println("Aligned doc: ${it.alignedDocument?.size ?: 0} bytes")
println("Photos: ${it.extractedPhotos.size}")
println("Fields: ${it.extraction?.fields?.size ?: 0}")
it.faceVerification?.let { fv ->
println("Face match: ${fv.isMatch}")
println("Similarity: ${fv.similarityScore}")
}
}
// Verso
result.versoResult?.let {
println("Verso score: ${it.score}")
println("Fields: ${it.extraction?.fields?.size ?: 0}")
}
// Extracted data (searches recto + verso)
println("Nom: ${result.getExtractedValue("nom")}")
println("NIN: ${result.getExtractedValue("nin")}")
// Loop all fields sorted by priority
result.rectoResult?.extraction?.sortedFields?.forEach { field ->
println("${field.label}: ${field.stringValue}")
}
import KyvshieldLite
// ── Possible values ───────────────────────────────────────────────────
// CaptureStep : .selfie, .recto, .verso
// ChallengeMode : .minimal, .standard, .strict
// SelfieDisplayMode : .standard, .compact, .immersive, .neonHud
// DocumentDisplayMode: .standard, .compact, .immersive, .neonHud
// ChallengeAudioRepeat: .once, .twice, .thrice
// ── Fetch documents from API ──────────────────────────────────────────
let url = URL(string: "https://kyvshield-naruto.innolinkcloud.com/api/v1/documents")!
var request = URLRequest(url: url)
request.setValue("YOUR_API_KEY", forHTTPHeaderField: "X-API-Key")
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let docsJson = json["documents"] as! [[String: Any]]
let docs = docsJson.map { KyvshieldDocument.fromJson($0) }
let selectedDoc = docs.first!
// ── Request camera permission ─────────────────────────────────────────
KyvshieldLite.requestCameraPermission { granted in
guard granted else {
if KyvshieldLite.isCameraPermissionDenied() {
KyvshieldLite.openSettings()
}
return
}
startKyc(doc: selectedDoc)
}
// ── Start KYC (UIKit) ─────────────────────────────────────────────────
func startKyc(doc: KyvshieldDocument) {
let config = KyvshieldConfig(
baseUrl: "https://kyvshield-naruto.innolinkcloud.com",
apiKey: "YOUR_API_KEY",
enableLog: true,
theme: KyvshieldThemeConfig(
primaryColor: UIColor(hex: "#EF8352"),
brightness: .light
)
)
let flow = KyvshieldFlowConfig(
steps: [.selfie, .recto, .verso],
target: doc,
kycIdentifier: "user-12345",
challengeMode: .minimal,
stepChallengeModes: [
.selfie: .minimal,
.recto: .standard,
.verso: .minimal,
],
selfieDisplayMode: .standard,
documentDisplayMode: .standard,
requireFaceMatch: true,
showIntroPage: true,
showInstructionPages: true,
showResultPage: true,
showSuccessPerStep: true,
language: "fr",
playChallengeAudio: true,
maxChallengeAudioPlay: .once,
pauseBetweenAudioPlayMs: 1000
)
KyvshieldLite.initKyc(from: self, config: config, flow: flow) { result in
self.handleResult(result)
}
}
// ── SwiftUI alternative ───────────────────────────────────────────────
// .fullScreenCover(isPresented: $showKyc) {
// KyvshieldView(config: config, flow: flow) { result in
// showKyc = false
// handleResult(result)
// }
// }
// ── Handle result ─────────────────────────────────────────────────────
func handleResult(_ result: KYCResult) {
print("Success: \(result.success)")
print("Status: \(result.overallStatus)") // .pass, .reject, .error
print("Session: \(result.sessionId ?? "")")
// Selfie
if let selfie = result.selfieResult {
print("Live: \(selfie.isLive)")
print("Image: \(selfie.capturedImage?.count ?? 0) bytes")
}
// Recto
if let recto = result.rectoResult {
print("Recto score: \(recto.score)")
print("Aligned doc: \(recto.alignedDocument?.count ?? 0) bytes")
print("Photos: \(recto.extractedPhotos.count)")
print("Fields: \(recto.extraction?.fields.count ?? 0)")
if let fv = recto.faceVerification {
print("Face match: \(fv.isMatch)")
print("Similarity: \(fv.similarityScore)")
}
}
// Verso
if let verso = result.versoResult {
print("Verso score: \(verso.score)")
print("Fields: \(verso.extraction?.fields.count ?? 0)")
}
// Extracted data (searches recto + verso)
print("Nom: \(result.getExtractedValue("nom") ?? "")")
print("NIN: \(result.getExtractedValue("nin") ?? "")")
// Loop all fields sorted by priority
for field in result.rectoResult?.extraction?.sortedFields ?? [] {
print("\(field.label): \(field.stringValue ?? "")")
}
}
import React, { useState } from 'react';
import { Button, View, Alert } from 'react-native';
import {
KyvshieldModal,
FlowPresets,
requestCameraPermission,
isCameraPermissionDenied,
openSettings,
kycResultGetExtractedValue,
} from '@kyvshield/react-native';
import type {
KyvshieldConfig,
KyvshieldFlowConfig,
KyvshieldDocument,
CaptureStep,
ChallengeMode,
SelfieDisplayMode,
DocumentDisplayMode,
KYCResult,
} from '@kyvshield/react-native';
// ── Possible values ───────────────────────────────────────────────────
// CaptureStep : 'selfie', 'recto', 'verso'
// ChallengeMode : 'minimal', 'standard', 'strict'
// SelfieDisplayMode : 'standard', 'compact', 'immersive', 'neonHud'
// DocumentDisplayMode: 'standard', 'compact', 'immersive', 'neonHud'
// ChallengeAudioRepeat: 'once', 'twice', 'thrice'
// ── Fetch documents from API ──────────────────────────────────────────
async function fetchDocuments(): Promise<KyvshieldDocument[]> {
const res = await fetch(
'https://kyvshield-naruto.innolinkcloud.com/api/v1/documents',
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } },
);
const { documents } = await res.json();
return documents;
}
// ── Config ────────────────────────────────────────────────────────────
const config: KyvshieldConfig = {
baseUrl: 'https://kyvshield-naruto.innolinkcloud.com',
apiKey: 'YOUR_API_KEY',
enableLog: true,
theme: {
primaryColor: '#EF8352',
darkMode: false,
},
};
// ── Flow ──────────────────────────────────────────────────────────────
const flow: KyvshieldFlowConfig = {
steps: ['selfie', 'recto', 'verso'] as CaptureStep[],
language: 'fr',
showIntroPage: true,
showInstructionPages: true,
showResultPage: true,
showSuccessPerStep: true,
selfieDisplayMode: 'standard' as SelfieDisplayMode,
documentDisplayMode: 'standard' as DocumentDisplayMode,
challengeMode: 'minimal' as ChallengeMode,
requireFaceMatch: true,
playChallengeAudio: true,
maxChallengeAudioPlay: 'once',
pauseBetweenAudioPlayMs: 1000,
target: selectedDoc,
kycIdentifier: 'user-12345',
};
// ── Component ─────────────────────────────────────────────────────────
export default function App() {
const [visible, setVisible] = useState(false);
const startKyc = async () => {
const granted = await requestCameraPermission();
if (!granted) {
const denied = await isCameraPermissionDenied();
if (denied) openSettings();
return;
}
setVisible(true);
};
const handleResult = (result: KYCResult) => {
setVisible(false);
console.log('Success:', result.success);
console.log('Status:', result.overallStatus); // pass, reject, error
console.log('Session:', result.sessionId);
// Selfie
if (result.selfieResult) {
console.log('Live:', result.selfieResult.isLive);
console.log('Image:', result.selfieResult.capturedImageBase64?.length, 'chars');
}
// Recto
if (result.rectoResult) {
console.log('Score:', result.rectoResult.score);
console.log('Fields:', result.rectoResult.extraction?.fields?.length);
if (result.rectoResult.faceVerification) {
console.log('Face match:', result.rectoResult.faceVerification.isMatch);
console.log('Similarity:', result.rectoResult.faceVerification.similarityScore);
}
}
// Extracted data
console.log('Nom:', kycResultGetExtractedValue(result, 'nom'));
console.log('NIN:', kycResultGetExtractedValue(result, 'nin'));
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Start KYC" onPress={startKyc} />
<KyvshieldModal
visible={visible}
config={config}
flow={flow}
onResult={handleResult}
onDismiss={() => setVisible(false)}
/>
</View>
);
}
API Reference
Common to all platforms
Methods
| Method | Returns | Description |
|---|---|---|
| initKyc(config, flow) | KycResult | Launch the full KYC flow. Returns result when user completes or cancels. |
| checkCameraPermission() | bool | Check and request camera permission. Returns true if granted. |
| isCameraPermissionPermanentlyDenied() | bool | Returns true if camera was permanently denied. User must go to Settings. |
| openSettings() | bool | Opens device settings so user can manually enable camera permission. |
config KyvshieldConfig
| Property | Type | Default | Description |
|---|---|---|---|
| baseUrl | String | required | API server URL |
| apiKey | String | required | API key from dashboard |
| apiVersion | String | 'v1' | API version |
| timeoutSeconds | int | 60 | HTTP request timeout (not session timeout) |
| enableLog | bool | false | Enable debug logs |
| theme | ThemeConfig? | null | Custom theme (see below) |
| acceptReviewStatus | bool | false | Accept REVIEW status as valid |
flow KyvshieldFlowConfig
| Property | Type | Default | Description |
|---|---|---|---|
| steps | CaptureStep[] | [selfie, recto, verso] | Steps to execute (order matters) |
| target | String? | null (auto-detect) | Document type: SN-CIN, SN-PASSPORT, SN-DRIVER-LICENCE |
| challengeMode | ChallengeMode | standard | minimal (1) | standard (3) | strict (5+) |
| language | String | 'fr' | fr | en | wo |
| showIntroPage | bool | true | Show intro screen before KYC |
| showInstructionPages | bool | true | Show instructions before each step |
| showResultPage | bool | true | Show result screen after KYC |
| showSuccessPerStep | bool | true | Show success animation after each step |
| requireFaceMatch | bool | true | Compare selfie vs document photo |
| selfieDisplayMode | DisplayMode | standard | standard | compact | immersive |
| documentDisplayMode | DisplayMode | standard | standard | compact | immersive |
| playChallengeAudio | bool | false | Play voice instructions for each challenge |
| maxChallengeAudioPlay | int | 1 | Repeat audio 1, 2 or 3 times |
| kycIdentifier | String? | null | Your reference ID (returned in webhooks) |
| strings | Strings? | null | Override any UI text |
Flow Presets (Flutter)
KyvshieldFlowConfig.selfieOnly()Selfie only, no document
KyvshieldFlowConfig.standard()Selfie + recto (recommended)
KyvshieldFlowConfig.full()Selfie + recto + verso
KyvshieldFlowConfig.quick()No UI pages, minimal challenges
KyvshieldFlowConfig.strict()Maximum security (5+ challenges)
KyvshieldFlowConfig.documentOnly()Recto + verso, no selfie
ThemeConfig
| Property | Type | Description |
|---|---|---|
| primaryColor | Color / String | Brand color (buttons, accents, indicators) |
| successColor | Color? | Success feedback color (default: #10B981) |
| warningColor | Color? | Warning color (default: #F59E0B) |
| errorColor | Color? | Error color (default: #EF4444) |
| themeMode | String | light | dark | auto |
Presets
#EF8352#3B82F6#10B981#8B5CF6#00377D#FFD100Localization & Custom Strings
3 built-in languages. Override any text via strings parameter.
Customizable string keys
Intro Screen
- introTitle
- introSubtitle
- introStartButton
- introDocTypeLabel
- introDocTypePrefix
Selfie Capture
- selfieStepTitle
- selfiePlaceFace
- selfieHoldStill
- selfieFaceDetected
- selfieNoFace
- selfieTooFar
- selfieTooClose
- selfieNotCentered
Document Capture
- stepRectoTitle
- stepVersoTitle
- capturePlaceCardInFrame
- captureDocumentDetected
- captureLookingForDocument
Challenges
- challengeCenterFace
- challengeCloseEyes
- challengeTurnLeft
- challengeTurnRight
- challengeSmile
- challengeLookUp
- challengeLookDown
Results
- resultLivenessSuccess
- resultLivenessFailed
- resultScoreLabel
- resultRetryPrompt
- buttonContinue
- buttonRetry
- buttonCancel
Analysis
- statusAnalyzing
- analyzingPleaseWait
- analysisStepImageCaptured
- analysisStepQualityVerified
- analysisStepSecurityCheck
- analysisStepAIVerification
enum Types & Enums
CaptureStep
selfieFace liveness verificationrectoDocument front side (OCR + fraud check)versoDocument back side (NIN + MRZ)ChallengeMode
minimal1-2 challenges (fastest)standard3-4 challenges (balanced, default)strict5+ challenges (max security)DisplayMode
standardCamera + instructions section belowcompactCamera + overlaid instructionsimmersiveFull-screen camera, floating overlaysSupported Documents
SN-CINSenegal ID Card (recto + verso)SN-PASSPORTSenegal Passport (recto only)SN-DRIVER-LICENCESenegal Driver License (recto + verso)result KYCResult
Returned by initKyc() / startKyc()
| Property | Type | Description |
|---|---|---|
| success | bool | KYC completed without error |
| overallStatus | VerificationStatus | pass | review | reject | error |
| sessionId | String? | Server session ID (for webhook correlation) |
| selfieResult | SelfieResult? | Liveness verification result |
| rectoResult | DocumentResult? | Front side OCR + fraud analysis |
| versoResult | DocumentResult? | Back side (NIN, MRZ) |
| fraudIndicators | String[] | Detected fraud signals |
| totalProcessingTimeMs | int | Total processing time |
| errorMessage | String? | Error description if failed |
| Convenience getters: | ||
| selfieImage | bytes? | Captured selfie image |
| rectoImage | bytes? | Aligned recto document image |
| versoImage | bytes? | Aligned verso document image |
| faceMatches | bool? | Selfie matches document photo |
| faceSimilarityScore | double? | Face match score (0-100%) |
| getExtractedValue(key) | String? | Get any OCR field by key |
| mainPhoto | ExtractedPhoto? | Face photo from document |
result SelfieResult
| Property | Type | Description |
|---|---|---|
| success | bool | Step completed successfully |
| isLive | bool | Real person detected (not photo/screen/mask) |
| confidence | double | Confidence score (0.0 - 1.0) |
| status | VerificationStatus | pass or reject |
| capturedImage | bytes? | Captured selfie image (JPEG) |
| challengesPassed | int | Number of challenges passed |
| challengesTotal | int | Total challenges assigned |
| userMessages | String[] | Localized messages for user |
| spoofingIndicators | String[] | Detected spoofing signals |
| processingTimeMs | int | Processing time in ms |
result DocumentResult
Used for both recto and verso
| Property | Type | Description |
|---|---|---|
| success | bool | Step completed successfully |
| isLive | bool | Real physical document (not screen/printout) |
| score | double | Authenticity score (0.0 - 1.0) |
| confidenceLevel | String | HIGH | MEDIUM | LOW |
| status | VerificationStatus | pass | review | reject |
| alignedDocument | bytes? | Perspective-corrected document image |
| extraction | DocumentData? | OCR extracted fields (see below) |
| extractedPhotos | ExtractedPhoto[] | Face photos extracted from document |
| faceVerification | FaceResult? | Selfie vs document face match |
| userMessages | String[] | Localized messages for user |
| fraudIndicators | String[] | Detected fraud signals |
| processingTimeMs | int | Processing time in ms |
data ExtractedField & DocumentData
DocumentData contains a sorted list of ExtractedField. Use getValue(key) or iterate sortedFields.
| Property | Type | Description |
|---|---|---|
| key | String | Generic key (document_id, first_name, birth_date...) |
| documentKey | String | Document-specific key (numero_carte, prenoms...) |
| label | String | Localized display label |
| value | any | Extracted value (String, array, or map) |
| displayPriority | int | Sort order (lower = show first) |
| icon | String? | Icon name (user, calendar, credit_card...) |
Convention displayPriority
- displayPriority = 1 → Toujours l'identifiant principal du document (N° carte pour le recto, NIN pour le verso)
- displayPriority = 2-10 → Champs importants (nom, prénom, date de naissance...)
- displayPriority = 99+ → Champs techniques (MRZ, codes internes)
Utilisez sortedFields pour afficher les champs dans l'ordre recommandé.
DocumentData methods
data ExtractedPhoto
| Property | Type | Description |
|---|---|---|
| imageBytes | bytes | Face photo (JPEG) |
| confidence | double | Detection confidence (0-1) |
| width | int | Image width px |
| height | int | Image height px |
| bbox | double[] | Bounding box [x1, y1, x2, y2] |
result FaceResult
Selfie vs document face match (inside DocumentResult.faceVerification)
| Property | Type | Description |
|---|---|---|
| isMatch | bool | Faces match (selfie = document photo) |
| similarityScore | double | Similarity percentage (0-100%) |
| threshold | double | Match threshold used |
| confidenceLevel | String | VERY_HIGH | HIGH | MEDIUM | LOW |
| detectionModel | String | Model used (e.g., scrfd_10g) |
| recognitionModel | String | Model used (e.g., buffalo_l) |
| processingTimeMs | int | Face comparison time in ms |
Documents Supportes
Champs extractibles par type de document
Carte d'Identite CEDEAO
SN-CIN
| Cle | Label | Format | Requis |
|---|---|---|---|
| numero_carte | N° Carte d'identite | X XXXX AAAA XXXXX | Oui |
| prenoms | Prenoms | Texte | Oui |
| nom | Nom | Texte | Oui |
| date_naissance | Date de naissance | JJ/MM/AAAA | Oui |
| sexe | Sexe | M / F | Oui |
| taille | Taille | X,XX m | Non |
| lieu_naissance | Lieu de naissance | Texte | Non |
| date_delivrance | Date de delivrance | JJ/MM/AAAA | Non |
| date_expiration | Date d'expiration | JJ/MM/AAAA | Oui |
| centre_enregistrement | Centre d'enregistrement | Texte | Non |
| adresse_domicile | Adresse du domicile | Texte | Non |
| birth_region | Region de naissance (derive) | Deduit | Auto |
| Cle | Label | Format | Requis |
|---|---|---|---|
| code_pays | Code Pays | SEN (fixe) | Oui |
| Informations electorales — presents si l'electeur est inscrit, sinon null | |||
| inscrit_liste_electorale | Inscrit sur la liste electorale | true / false | Non |
| numero_electeur | Numero d'electeur | chiffres | Non |
| region | Region | Texte | Non |
| departement | Departement | Texte | Non |
| arrondissement | Arrondissement | Texte | Non |
| commune | Commune | Texte | Non |
| lieu_de_vote | Lieu de vote | Texte | Non |
| bureau_de_vote | Bureau de vote | Numero | Non |
| nin | NIN (Numero d'Identification Nationale) | 13 chiffres sans espaces | Oui |
| mrz | MRZ (Machine Readable Zone) | 3 lignes x 30 chars | Oui |
Structure MRZ (ICAO 9303)
Passeport Biometrique
SN-PASSPORT
| Cle | Label | Format | Requis |
|---|---|---|---|
| numero_passeport | N° Passeport | AXXXXXXXX | Oui |
| nom | Nom | UPPERCASE | Oui |
| prenoms | Prenoms | UPPERCASE | Oui |
| nationalite | Nationalite | SENEGALAISE | Oui |
| date_naissance | Date de naissance | DD MMM YYYY | Oui |
| nin | N° personnel (NIN) | 13 chiffres | Oui |
| sexe | Sexe | M / F | Oui |
| lieu_naissance | Lieu de naissance | Texte | Oui |
| autorite | Autorite | Texte | Oui |
| date_delivrance | Date de delivrance | DD MMM YYYY | Oui |
| date_expiration | Date d'expiration | DD MMM YYYY | Oui |
| mrz | MRZ (Machine Readable Zone) | 2 lignes x 44 chars | Oui |
Structure MRZ Passeport (ICAO 9303 TD3)
Caracteristiques
- Format: ID-3 (125mm x 88mm)
- Pages: 48
- Materiau: Polycarbonate
- Photo: Couleur, fond bleu
Cross-validation CIN
- NIN doit correspondre au verso CIN
- Nom/Prenoms identiques
- Date de naissance identique
- Sexe identique
Permis de Conduire
SN-DRIVER-LICENCE
| Cle | Label | Format | Requis |
|---|---|---|---|
| numero_permis | N° Permis | XXXXXXXX | Oui |
| nom | Nom | Texte | Oui |
| prenoms | Prenoms | Texte | Oui |
| date_naissance | Date et lieu de naissance | DD/MM/YYYY + Ville | Oui |
| date_emission | Date d'emission | DD/MM/YYYY | Oui |
| date_expiration | Date d'expiration | DD/MM/YYYY | Oui |
| delivre_par | Delivre par | MITTD | Oui |
| categories | Categories | A1 B etc | Oui |
| groupe_sanguin | Groupe sanguin | O+ A- B+ | Non |
| Cle | Label | Format | Requis |
|---|---|---|---|
| adresse | Adresse | Texte | Oui |
| commune | Commune | Texte | Oui |
| departement | Departement | Texte | Oui |
| region | Region | Texte | Oui |
| sexe | Sexe | M / F | Oui |
| categories_actives | Categories actives (derive) | ["A1", "B"] | Auto |
| mrz | MRZ | D1SN... | Oui |
Tableau des Categories (colonnes 9-12)
Structure MRZ Permis (1 ligne)
Modeles IA - InsightFace
Tous les modeles utilisent ONNX Runtime pour des performances optimales.
Modeles de Detection (SCRFD)
| Modele | FLOPs | Vitesse | Precision | Usage |
|---|---|---|---|---|
| scrfd_10g | 10G | 4.9ms | 95.16% | Production |
| scrfd_2.5g | 2.5G | 4.2ms | 93.78% | Balance |
| scrfd_500m | 500M | ~2ms | ~90% | Mobile |
Modeles de Reconnaissance (ArcFace / GLint)
Glossaire
Backbones (Architectures)
-
ResNet50- Reseau de neurones a 50 couches. Bon equilibre precision/vitesse. -
ResNet100- Reseau a 100 couches. Plus precis mais plus lent. -
MobileFaceNet- Architecture legere optimisee pour mobile. Tres rapide.
Datasets (Donnees d'entrainement)
-
WebFace600K- 600 000 identites, ~10M images de visages. -
GLint360K- 360 000 identites, 17M images. Dataset plus large. -
MS1MV2- MS-Celeb-1M nettoye. ~5.8M images, 85K identites.
Convention de nommage des modeles
w600k_r50= WebFace600K + ResNet50 (ArcFace standard)w600k_mbf= WebFace600K + MobileFaceNet (version mobile)glintr100= GLint360K + ResNet100 (haute precision)
| Pack | Modele | Backbone | Dataset | LFW | Usage |
|---|---|---|---|---|---|
| buffalo_l | w600k_r50 | ResNet50 | MS1MV2 (600K ids) | 99.8% | Production |
| buffalo_s | w600k_mbf | MobileFaceNet | MS1MV2 (600K ids) | 99.5% | Mobile / Fast |
| antelopev2 | glintr100 | ResNet100 | GLint360K (17M imgs) | 99.8% | Alternative |
Recommandations par region
Face Verification Playground
Testez l'API de verification faciale.
Visage de reference
Selfie ou photo
Verification en cours...
Uploadez deux images et cliquez sur Verifier
Face Detection Playground
Detectez les visages dans une image et obtenez les bounding boxes.
Glissez ou cliquez pour uploader
Detection en cours...
Uploadez une image et cliquez sur Detecter
Cles API Temporaires
Generez des cles API temporaires pour vos SDK mobiles. Les cles temporaires heritent des permissions de la cle parente et expirent automatiquement. Seules les cles principales peuvent generer/revoquer des cles temporaires.
Securite
- • Les cles temporaires ne peuvent PAS generer d'autres cles
- • Les cles temporaires ne peuvent PAS revoquer d'autres cles
- • Seules les cles principales (master) ont ces privileges
- • Les cles expirent automatiquement (max 24h)
Documents API
Listez les documents d'identite supportes avec leurs champs, elements visuels et de securite.
Chargement des documents...
Cliquez sur Charger (API Key requise ci-dessus)
Usage API
Consultez les statistiques d'utilisation de votre application avec suivi mensuel.
Retourne les statistiques d'utilisation a deux niveaux:
- application_usage: Usage global de l'application (toutes les cles confondues)
- api_keys_usage: Usage par cle API master (pas les cles temporaires)
Chargement des statistiques...
Cliquez sur Charger (API Key requise ci-dessus)
Compteurs disponibles
Webhooks
Recevez des notifications en temps reel apres chaque etape et a la fin de chaque session KYC.
Configuration
Configurez votre webhook dans les settings de votre application:
{
"settings": {
"webhook_url": "https://your-api.com/kyvshield/webhook"
}
}
Note: La signature HMAC utilise l'API key qui a initie la session comme secret.
Headers HTTP
POST https://your-api.com/kyvshield/webhook
Host your-api.com
x-kyvshield-timestamp: 2026-03-15T04:11:36Z
x-kyvshield-session: 468d970da539365c7f704ebd8f246cb4
x-kyvshield-event: selfie.completed
x-kyvshield-signature: sha256=<HMAC-SHA256 du body avec API key>
content-type: application/json
user-agent: KyvShield-Webhook/1.0
Evenements
selfie.completed
Selfie verifie avec succes
recto.completed
Recto analyse avec succes
verso.completed
Verso analyse avec succes
session.completed
Session KYC terminee avec succes
selfie.failed
Selfie rejete (non vivant/fraude)
recto.failed
Recto rejete (fraude detectee)
verso.failed
Verso rejete (fraude detectee)
session.failed
Session echouee ou timeout
selfie.completed Liveness
Envoye apres la verification liveness du selfie.
{
"event": "selfie.completed",
"timestamp": "2026-03-15T04:11:36Z",
"session_id": "468d970da539365c7f704ebd8f246cb4",
"app_id": "demo_app",
"key_id": "demo_key_1",
"document_type": "SN-CIN",
"step_data": {
"captured_image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...<base64>...",
"liveness": {
"confidence": "HIGH",
"is_live": true,
"score": 0.95
},
"processing_time_ms": 8907,
"step_index": 0,
"step_type": "selfie",
"success": true,
"verification": {
"checks_passed": ["center_face", "close_eyes"],
"confidence": 0.95,
"fraud_indicators": [],
"is_authentic": true
}
}
}
recto.completed OCR + Fraud
Envoye apres l'analyse du recto du document d'identite. Inclut OCR, detection de visage, et verification anti-fraude.
{
"event": "recto.completed",
"timestamp": "2026-03-15T04:57:25Z",
"session_id": "5b5f09cd882170f81889dc9448bf43c6",
"app_id": "demo_app",
"key_id": "demo_key_1",
"document_type": "SN-CIN",
"step_data": {
"aligned_document": "/9j/4AAQSkZJRgABAQAAAQABAAD/...<base64 JPEG>...",
"detected_faces": [
{
"image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...<base64>...",
"confidence": 0.828,
"bbox": [96, 222, 210, 373],
"area": 17214,
"width": 114,
"height": 151
}
],
"extracted_photos": [
{
"image": "/9j/4AAQSkZJRgABAQAAAQABAAD/...<base64 JPEG>...",
"confidence": 0.95,
"bbox": [96, 222, 210, 373],
"area": 17214,
"width": 114,
"height": 151
}
],
"extraction": [
{"key": "document_id", "document_key": "numero_carte", "label": "N° de la carte", "value": "1 06 19930515 00026 8"},
{"key": "first_name", "document_key": "prenoms", "label": "Prenoms", "value": "MOUSSA"},
{"key": "last_name", "document_key": "nom", "label": "Nom", "value": "NDOUR"},
{"key": "birth_date", "document_key": "date_naissance", "label": "Date de naissance", "value": "15/05/1993"},
{"key": "sex", "document_key": "sexe", "label": "Sexe", "value": "M"},
{"key": "height", "document_key": "taille", "label": "Taille", "value": "175 cm"},
{"key": "birth_place", "document_key": "lieu_naissance", "label": "Lieu de naissance", "value": "KAOLACK"},
{"key": "issue_date", "document_key": "date_delivrance", "label": "Date de delivrance", "value": "29/07/2019"},
{"key": "expiry_date", "document_key": "date_expiration", "label": "Date d'expiration", "value": "28/07/2029"},
{"key": "issuing_authority", "document_key": "centre_enregistrement", "label": "Centre", "value": "COMM. DE DIEUPPEUL"}
],
"liveness": {
"confidence": "HIGH",
"is_live": true,
"score": 0.92
},
"processing_time_ms": 11631,
"step_type": "recto",
"success": true,
"verification": {
"checks_passed": ["center_document", "tilt_left", "tilt_right"],
"confidence": 0.92,
"fraud_indicators": [],
"is_authentic": true
}
}
}
Note: aligned_document = image redresse, detected_faces = visages detectes par ML, extracted_photos = photos extraites du document (pour face match), extraction = donnees OCR.
verso.completed NIN + MRZ
Envoye apres l'analyse du verso. Contient le NIN et le MRZ.
{
"event": "verso.completed",
"timestamp": "2026-03-15T05:00:21Z",
"session_id": "d59dc22f9e792aba85637150aef25cee",
"app_id": "demo_app",
"key_id": "demo_key_1",
"document_type": "SN-CIN",
"step_data": {
"aligned_document": "/9j/4AAQSkZJRgABAQAAAQABAAD/...<base64>...",
"extraction": [
{"key": "code_pays", "label": "Code Pays", "value": "SEN"},
{"key": "inscrit_liste_electorale", "label": "Inscrit liste electorale", "value": true},
{"key": "numero_electeur", "label": "Numero d'electeur", "value": "100656150"},
{"key": "region", "label": "Region", "value": "DAKAR"},
{"key": "departement", "label": "Departement", "value": "GUEDIAWAYE"},
{"key": "arrondissement", "label": "Arrondissement", "value": "GUEDIAWAYE"},
{"key": "commune", "label": "Commune", "value": "GOLF SUD"},
{"key": "lieu_de_vote", "label": "Lieu de vote", "value": "LYCEE PARCELLES ASSAINIES GUEDIAWAY"},
{"key": "bureau_de_vote", "label": "Bureau de vote", "value": "01"},
{"key": "national_id", "document_key": "nin", "label": "NIN", "value": "1765198311101"},
{"key": "mrz", "label": "MRZ", "value": "I<SEN101198304<...<3 lignes MRZ>..."}
],
"liveness": {
"confidence": "HIGH",
"is_live": true,
"score": 0.92
},
"processing_time_ms": 12308,
"step_type": "verso",
"success": true,
"verification": {
"checks_passed": ["center_document"],
"confidence": 0.92,
"fraud_indicators": [],
"is_authentic": true
}
}
}
session.completed Final
Envoye quand toute la session KYC est terminee avec succes.
{
"event": "session.completed",
"timestamp": "2026-03-15T04:11:36Z",
"session_id": "468d970da539365c7f704ebd8f246cb4",
"app_id": "demo_app",
"key_id": "demo_key_1",
"document_type": "SN-CIN",
"final_result": {
"success": true,
"status": "PASS",
"steps": {
"face_match_passed": true,
"face_match_score": 0.87,
"overall_confidence": 0.95,
"processing_time_ms": 8907,
"rejection_reason": "",
"steps_completed": 3,
"steps_failed": 0,
"steps_passed": 3
}
}
}
session.failed Erreur
Envoye quand la session expire ou echoue.
{
"event": "session.failed",
"timestamp": "2026-03-15T04:09:45Z",
"session_id": "...",
"app_id": "demo_app",
"key_id": "demo_key_1",
"document_type": "SN-CIN",
"final_result": {
"success": false,
"status": "TIMEOUT",
"steps": {
"reason": "session_expired"
}
}
}
Verification de signature HMAC
Le header X-KyvShield-Signature contient la signature HMAC-SHA256 du body JSON.
Le secret est l'API key qui a cree la session.
import hmac
import hashlib
def verify_signature(payload_bytes: bytes, api_key: str, signature_header: str) -> bool:
"""
Verifie la signature HMAC du webhook.
Args:
payload_bytes: Le body JSON brut du webhook
api_key: L'API key qui a cree la session
signature_header: La valeur du header X-KyvShield-Signature
Returns:
True si la signature est valide
"""
# Le header est au format "sha256=<hex>"
if not signature_header.startswith("sha256="):
return False
expected_sig = signature_header[7:] # Remove "sha256=" prefix
# Calcul du HMAC-SHA256
computed = hmac.new(
api_key.encode('utf-8'),
payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, expected_sig)
# Usage Flask/FastAPI:
# @app.post("/webhook")
# def handle_webhook(request: Request):
# signature = request.headers.get("X-KyvShield-Signature", "")
# if not verify_signature(request.body(), "your_api_key", signature):
# raise HTTPException(401, "Invalid signature")
# data = request.json()
# # Process webhook...
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
// VerifySignature verifie la signature HMAC du webhook
func VerifySignature(payloadBytes []byte, apiKey, signatureHeader string) bool {
// Le header est au format "sha256=<hex>"
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
expectedSig := signatureHeader[7:] // Remove "sha256=" prefix
// Calcul du HMAC-SHA256
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write(payloadBytes)
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(expectedSig))
}
const crypto = require('crypto');
function verifySignature(payloadBuffer, apiKey, signatureHeader) {
// Le header est au format "sha256=<hex>"
if (!signatureHeader.startsWith('sha256=')) {
return false;
}
const expectedSig = signatureHeader.slice(7);
// Calcul du HMAC-SHA256
const computed = crypto
.createHmac('sha256', apiKey)
.update(payloadBuffer)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expectedSig)
);
}
// Usage Express:
// app.post('/webhook', express.raw({type: '*/*'}), (req, res) => {
// const sig = req.headers['x-kyvshield-signature'];
// if (!verifySignature(req.body, 'your_api_key', sig)) {
// return res.status(401).send('Invalid signature');
// }
// const data = JSON.parse(req.body);
// // Process webhook...
// });
Politique de retry
- • Max 3 tentatives avec backoff exponentiel (1s, 5s, 30s)
- • Timeout de 10 secondes par requete
- • Les echecs sont logues et conserves 24h
- • Votre endpoint doit retourner un code 2xx pour confirmer la reception