Développez avec Zoi ID
Deux API sur un même hôte, des clients typés dans six langages, un parcours hébergé et une sandbox. Le guide complet est déjà consultable, et les clés sandbox arrivent avec l'accès anticipé.
URL de basehttps://kyc.zoi.network
Démarrage rapide
- 1
Créez une session
Votre backend appelle
POST /v1/partner/sessionsavec votre clé partenaire. - 2
Lancez le parcours
Passez le jeton client au SDK, ou ouvrez
verificationUrldans un navigateur. - 3
Recevez le résultat
Vérifiez la signature du webhook, puis enregistrez l'identifiant de l'identité et son statut.
curl https://kyc.zoi.network/v1/partner/sessions \
-H "Authorization: Bearer $ZOI_ID_PARTNER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-user_42" \
-d '{ "externalUserId": "user_42", "level": "standard", "appId": "com.example.app" }'Deux API
API partenaire
De serveur à serveur, sous /v1/partner. Envoyez votre clé partenaire comme jeton Bearer. Ne la mettez jamais dans une appli.
API client
Utilisée par le SDK, sous /v1/client. Elle envoie un jeton client valable pour une session pendant 60 minutes.
Conventions
- Les heures sont en millisecondes epoch, dans des champs dont le nom se termine par
At. - Les dates sont au format
YYYY-MM-DD. Les parties inconnues sontXX, par exemple1990-XX-XX. - Les requêtes POST qui créent ou décident quelque chose acceptent un en-tête
Idempotency-Key. Les clés sont gardées 24 heures. - Les changements sur une session envoient la révision attendue. Si elle a changé, vous recevez
revision_conflict.
Déroulé de session
Votre backend crée la session. Le SDK fait les étapes. Le serveur vérifie l'ordre et répond step_not_allowed quand une étape arrive trop tôt.
1. Créez une session depuis votre backend
Un nouvel appel pour le même utilisateur reprend la session ouverte et donne un nouveau jeton client.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-signup-42
{
"externalUserId": "user_42",
"level": "standard",
"locale": "en",
"appId": "com.example.app"
}
200 OK
{
"sessionId": "ses_...",
"clientToken": "...",
"clientTokenExpiresAt": 1767225600000,
"status": "created",
"apiBaseUrl": "https://kyc.zoi.network",
"resumed": false
}2. Les étapes du SDK
- 01
Lire la session et les étapes encore nécessaires.
GET /v1/client/session
- 02
Enregistrer l'accord, avec le choix séparé et facultatif sur la comparaison du visage.
POST /v1/client/consent
- 03
Vérifier l'appli et l'appareil. Le résultat compte dans la décision.
POST /v1/client/attestation/challenge, POST /v1/client/attestation
- 04
Envoyer le document : lire la puce après un scan de la MRZ, ou envoyer des photos.
POST /v1/client/document/mrz-scan, POST /v1/client/document/chip/challenge, POST /v1/client/document/chip or POST /v1/client/document/photos
- 05
Filmer une courte vidéo avec la caméra avant pendant que l'appli montre les mouvements de tête.
POST /v1/client/liveness/challenge, POST /v1/client/liveness
- 06
Envoyer le pays de résidence, et l'adresse au niveau renforcé.
POST /v1/client/profile
- 07
Seulement avec l'exigence adresse : envoyer un document, puis confirmer l'adresse. Étape sautée quand la pièce d'identité l'a déjà fournie.
POST /v1/client/address/document, POST /v1/client/address/confirm
- 08
Seulement avec une exigence sur les fonds : enregistrer les réponses, ajouter des documents si la politique les demande, puis terminer l'étape.
POST /v1/client/funds/answers, POST /v1/client/funds/document, POST /v1/client/funds/remove, POST /v1/client/funds/complete
- 09
Répondre à deux questions : personne politiquement exposée, et agit pour son propre compte.
POST /v1/client/declarations
- 10
Soumettre avec la révision actuelle.
POST /v1/client/submit
L'utilisateur peut aussi demander qu'une personne examine un résultat automatique (POST /review-request) ou retirer son accord, ce qui annule la session et supprime ses fichiers tout de suite (POST /withdraw).
Justificatif de domicile et origine des fonds
Ajoutez requirements à POST /v1/partner/sessions pour vérifier plus que l'identité. address demande un justificatif de domicile. source_of_funds demande d'où vient l'argent, et source_of_wealth demande aussi le patrimoine total et la façon dont il s'est constitué. Les deux valeurs sur les fonds forment ensemble une seule étape, et toutes deux se combinent avec address.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
{
"externalUserId": "user_42",
"level": "standard",
"appId": "com.example.app",
"requirements": ["address", "source_of_funds"],
// Optional. Without them your partner default applies, else ours.
"addressPolicy": { "maxAgeDays": 90, "signedOnly": false },
"fundsPolicy": { "evidence": "optional", "currency": "EUR" }
}Politiques facultatives
Sans politique, votre réglage partenaire par défaut s'applique, sinon le nôtre. La politique est copiée sur la session à sa création.
addressPolicy:maxAgeDays(90 par défaut, de 1 à 366),acceptedTypes(par défaut : documents d'énergie, de télécom et de banque, documents fiscaux, courriers officiels et certificats de résidence) etsignedOnly(false par défaut).fundsPolicy:questions,evidence(none,optionalourequired,nonepar défaut),acceptedTypes,maxAgeDays(180 par défaut),currency(ISO 4217, USD par défaut) etbracketspour les tranches.- Une session ouverte avec d'autres exigences, pas encore soumise, est annulée et une nouvelle démarre, comme lors d'un changement de niveau.
- Testez chaque résultat avec des scénarios de sandbox comme
address_signed_pdf,address_document_review,funds_signed_documentsetfunds_review.
Sessions de suivi
Vérifiez uniquement les exigences pour un utilisateur dont l'identité est déjà active chez vous. Envoyez reuseIdentity: true avec requirements.
- L'utilisateur donne son consentement et ne fait que les nouvelles étapes. Aucun document, selfie ou déclaration n'est redemandé.
- En cas d'approbation, l'identité garde son identifiant, sa date de vérification et sa validité, et reçoit le nouveau résultat. Vous recevez
session.approvedetidentity.updated. - Sans identité active, l'appel répond 409
step_not_allowed. - Déclenchez-la depuis vos propres règles de risque, comme un dépôt important, un signalement PEP ou un pays à risque. Vos règles restent dans votre backend, au même endroit.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: edd-user_42-2026-11
{
"externalUserId": "user_42",
"appId": "com.example.app",
"requirements": ["source_of_funds", "source_of_wealth"],
"reuseIdentity": true,
"fundsPolicy": { "evidence": "required" }
}
// No active identity for user_42:
409 { "error": { "code": "step_not_allowed", ... } }Résultats et permissions
Les sessions approuvées portent address et funds, et l'identité garde le plus récent de chacun. Les champs de synthèse viennent avec toutes les permissions. Les détails exigent une permission.
- Permission
address: les lignes de l'adresse. Sans elle, vous recevezverified,method,evidenceLevel,documentType,documentDate,verifiedAtetcountry. - Permission
financial_data:answers,documentsetfindings. Sans elle, vous recevezkinds,evidenceLevel,reviewed,documentCountetverifiedAt. Elle ne fait jamais partie deeligibility. evidenceLevelvauthigh,medium,lowounone. Méthodes d'adresse, de la plus solide à la plus faible :wallet,id_document,signed_pdf,official_code,document_reviewetdeclared.- Les documents ne passent pas par l'API. Les membres qui ont le rôle conformité peuvent consulter les pages des documents sur l'origine des fonds dans le tableau de bord, si l'environnement a la permission
financial_data. Chaque consultation est journalisée.
"session": {
"id": "ses_...",
"status": "approved",
"requirements": ["address", "source_of_funds"],
"reusesIdentity": false,
"address": {
"verified": true,
"method": "signed_pdf",
"evidenceLevel": "high",
"documentType": "bank_statement",
"documentDate": "2026-10-14",
"verifiedAt": 1767225600000,
"country": "DE",
// scope "address"
"address": { "line1": "Musterstrasse 12", "city": "Berlin", "postalCode": "10115", "country": "DE" }
},
"funds": {
"kinds": ["source_of_funds"],
"evidenceLevel": "medium",
"reviewed": true,
"documentCount": 2,
"verifiedAt": 1767225600000,
// scope "financial_data"
"answers": {
"employmentStatus": "employed",
"income": { "min": 50000, "max": 100000, "currency": "EUR" },
"sources": ["salary", "savings"],
...
},
"documents": [
{ "documentType": "payslip", "documentDate": "2026-10-31", "amount": 5200, "currency": "EUR",
"signature": "valid", "signedByKnownIssuer": true, "nameMatch": "match", "findings": [], ... },
...
],
"findings": []
}
}Parcours web
Pas de SDK ? Envoyez l'utilisateur sur notre page hébergée. Elle utilise la même API, les mêmes contrôles et la même file de revue.
1. Obtenir le lien
POST /v1/partner/sessions renvoie verificationUrl. Passez redirectUrl pour renvoyer l'utilisateur chez vous à la fin. L'URL doit commencer par une de vos origines de redirection enregistrées.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
{
"externalUserId": "user_42",
"level": "standard",
"locale": "en",
"appId": "com.example.app",
"redirectUrl": "https://example.com/kyc/done"
}
200 OK
{
"sessionId": "ses_...",
"clientToken": "...",
"verificationUrl": "https://kyc.zoi.network/verify/#token=...&lang=en",
...
}- Le jeton client est dans le fragment de l'URL, après
#. Les navigateurs n'envoient jamais le fragment à un serveur, donc le jeton n'apparaît ni dans les journaux ni dans l'en-têteReferer. La page le lit, le retire de la barre d'adresse et le garde seulement en mémoire. - Ajoutez
langau fragment pour choisir une des 19 langues. Sans lui, la page utilise la langue du navigateur. - Un rechargement perd le jeton. Créez un nouveau lien pour la même session depuis votre backend.
- La page envoie
channel: "web"avec le consentement. Les navigateurs ne peuvent pas lire les puces des pièces d'identité : les sessions web utilisent donc des photos du document ou un portefeuille d'identité numérique. Elles sautent le contrôle de l'appareil, et les photos de documents envoyées depuis le web sont toujours tranchées par un examinateur formé. - Sur un ordinateur, la page affiche un code QR pour que l'utilisateur continue sur un téléphone.
2. Quand l'utilisateur a fini
Quand le parcours se termine (envoyé, annulé ou statut final), la page publie ce message vers chacun de ces objets qui existe : window.ReactNativeWebView, window.webkit.messageHandlers.zoiId, window.ZoiIdAndroid et window.opener. Puis elle ouvre redirectUrl avec zoiIdSession et zoiIdStatus ajoutés.
Ne faites jamais confiance au message ni aux paramètres de redirection. Faites confiance seulement à votre webhook ou à GET /v1/partner/sessions/:id.
// Sent to each bridge that exists:
// window.ReactNativeWebView.postMessage(JSON string)
// window.webkit.messageHandlers.zoiId.postMessage(object)
// window.ZoiIdAndroid.postMessage(JSON string)
// window.opener.postMessage(object, "<origin of your redirectUrl>")
// (popups only, and only when the session has a redirectUrl)
{
"source": "zoi-id",
"type": "finished", // or "cancelled"
"sessionId": "ses_...",
"status": "processing" // the session status at that moment
}
// Then, if redirectUrl was set (the page uses the one stored with the
// session, never one from the address bar):
https://example.com/kyc/done?zoiIdSession=ses_...&zoiIdStatus=processing3. Réglages de la webview
Android WebView
Activez javaScriptEnabled et domStorageEnabled, et mettez mediaPlaybackRequiresUserGesture à false. Dans un WebChromeClient, accordez onPermissionRequest pour RESOURCE_VIDEO_CAPTURE une fois que votre app a la permission CAMERA. Ajoutez un sélecteur de fichiers pour la solution de secours par photo.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// Only after your app holds Manifest.permission.CAMERA.
if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
} else {
request.deny()
}
}
override fun onShowFileChooser(
view: WebView, callback: ValueCallback<Array<Uri>>, params: FileChooserParams,
): Boolean {
// Open a picker and pass the result to callback (photo fallback).
return true
}
}iOS WKWebView
iOS 14.3 ou plus récent. Mettez allowsInlineMediaPlayback à true et ajoutez NSCameraUsageDescription à votre app. Depuis iOS 15, vous pouvez décider de la permission caméra dans le délégué UI.
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self // iOS 15+: decide camera access below
func webView(_ webView: WKWebView,
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(origin.host == "kyc.zoi.network" ? .grant : .deny)
}
// Info.plist: NSCameraUsageDescriptionReact Native WebView
Définissez mediaCapturePermissionGrantType="grant", allowsInlineMediaPlayback et javaScriptEnabled. Votre app a besoin de la permission caméra.
<WebView
source={{ uri: verificationUrl }}
javaScriptEnabled
allowsInlineMediaPlayback
mediaCapturePermissionGrantType="grant"
onMessage={(event) => {
const message = JSON.parse(event.nativeEvent.data);
if (message.source === "zoi-id") {
// Close the webview, then ask your backend for the real status.
}
}}
/>Les iframes ne sont pas prises en charge, à cause des permissions caméra et du clickjacking. Utilisez un onglet, une fenêtre popup ou une webview.
Statuts
Session
created- La session existe, rien n'est encore envoyé.
in_progress- Accord donné, les étapes sont en cours d'envoi.
processing- Soumise, les contrôles tournent.
review- En attente d'un examinateur formé.
approved- Final. L'identité est vérifiée.
rejected- Final. Voir les codes de motif.
resubmit- L'utilisateur doit refaire les étapes de requiredSteps.
expired- Pas de décision finale à temps.
cancelled- Annulée par le partenaire, l'utilisateur ou une suppression.
Identité
active- Utilisable.
suspended- En attente, par exemple après une nouvelle correspondance avec une liste de sanctions.
expired- Le document ou la période de vérification a pris fin. Une nouvelle session vérifie à nouveau.
revoked- Final.
SDK
Le SDK React Native (Expo) vous donne un client API typé, la lecture de puce NFC et le contrôle de l'appareil. Il est headless : votre appli garde le contrôle total de ses écrans et de sa caméra. Côté serveur, utilisez les clients typés pour TypeScript, Python, Go, PHP, Java et Ruby.
import { ZoiIdClient, nextStep } from "@zoi/id-sdk";
// clientToken and apiBaseUrl come from your backend (POST /v1/partner/sessions).
const client = new ZoiIdClient({ baseUrl: apiBaseUrl, clientToken });
const session = await client.getSession();
switch (nextStep(session)) {
case "consent": /* show the notice and the biometric choice */ break;
case "document": /* chip read or photos */ break;
case "liveness": /* record the video */ break;
case "address": /* upload a document, then confirm the address */ break;
case "funds": /* answers, then documents when the policy asks */ break;
case "submit": await client.submit(session.revision); break;
case "done": /* wait for your backend to get the webhook */ break;
}Webhooks
Nous envoyons un webhook quand une session ou une identité change. Vérifiez toujours la signature avant de faire confiance au contenu.
- Chaque webhook porte un en-tête
Zoi-Id-Signature:t=unix seconds,v1=hex. v1est le HMAC-SHA256 du textet.raw bodyavec votre secret de webhook. Utilisez le corps brut, avant toute lecture du JSON.- Pendant un changement de secret, l'en-tête porte un
v1par secret actif. Acceptez le webhook si l'un d'eux correspond. - Refusez-le si
test à plus de 300 secondes de maintenant. - Les envois ratés sont refaits pendant 72 heures. Chaque nouvel essai est signé à nouveau avec un nouveau
t. - Les identités portent une
revision. Appliquez un événement seulement si sa révision est plus récente que celle que vous avez gardée.
POST <your webhook URL>
Zoi-Id-Signature: t=1767225600,v1=5f2b...e9
{
"v": 1,
"eventId": "evt_...",
"type": "identity.suspended",
"createdAt": 1767225600000,
"object": "identity",
"externalUserId": "user_42",
"identity": {
"identityId": "idn_...",
"revision": 7,
"status": "suspended",
"level": "standard",
"verifiedAt": 1764547200000,
"validUntil": 1859155200000,
"ageOver18": true,
"captureMethod": "nfc"
}
}Types d'événements
session.processingsession.reviewsession.approvedsession.rejectedsession.resubmitsession.expiredsession.cancelledidentity.updatedidentity.suspendedidentity.reinstatedidentity.expiredidentity.revoked
Vérifier la signature en TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyZoiIdSignature(
rawBody: string,
header: string | null,
secret: string,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
if (!header) return false;
let t: string | undefined;
const signatures: Buffer[] = [];
for (const part of header.split(",")) {
const [key, value] = part.trim().split("=");
if (!value) continue;
if (key === "t") t = value;
if (key === "v1" && /^[0-9a-f]+$/i.test(value)) signatures.push(Buffer.from(value, "hex"));
}
if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected));
}
// Example handler: read the raw body first, parse JSON only after the check.
export async function POST(request: Request) {
const rawBody = await request.text();
const ok = verifyZoiIdSignature(rawBody, request.headers.get("Zoi-Id-Signature"), process.env.ZOI_ID_WEBHOOK_SECRET!);
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(rawBody);
// Store event.eventId to skip duplicates, then apply it if the revision is newer.
return new Response(null, { status: 204 });
}Vérifier la signature en Go
package zoiid
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
const toleranceSeconds = 300
// VerifySignature checks the Zoi-Id-Signature header against the raw body.
func VerifySignature(rawBody []byte, header, secret string, now time.Time) bool {
var t string
var sigs [][]byte
for _, part := range strings.Split(header, ",") {
key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok || value == "" {
continue
}
switch key {
case "t":
t = value
case "v1":
if b, err := hex.DecodeString(value); err == nil {
sigs = append(sigs, b)
}
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || len(sigs) == 0 {
return false
}
if d := now.Unix() - ts; d > toleranceSeconds || d < -toleranceSeconds {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(rawBody)
expected := mac.Sum(nil)
for _, sig := range sigs {
if hmac.Equal(sig, expected) {
return true
}
}
return false
}Flux d'événements
GET /v1/partner/events renvoie tous les événements des 30 derniers jours, du plus ancien au plus récent. Passez after avec le dernier nextCursor pour lire la page suivante. Utilisez-le après une longue panne, ou pour vérifier qu'il ne vous manque rien.
GET /v1/partner/events?after=<nextCursor>&limit=100
Authorization: Bearer <partner key>
200 OK
{
"items": [ /* webhook payloads, oldest first */ ],
"nextCursor": "..." // null when there is nothing more
}Signalements de fraude
Prévenez-nous quand une session tranchée s'avère frauduleuse, par exemple après une rétrofacturation. Les signalements alimentent notre mesure des approbations erronées. Ils ne changent ni la session, ni l'identité, ni le résultat de l'utilisateur.
- Permission
sessions:write. Seulement pour les sessions tranchées (approuvées ou refusées). Avant une décision, l'appel répond 409. - Un corps vide signale
confirmed_fraudavec la catégorieother.statuspeut aussi valoirsuspected_fraud, ouclearedpour retirer votre propre signalement. - Catégories :
forged_document,impostor,deepfake_or_replay,synthetic_identity,account_takeoveretother. Pas de catégorie aveccleared. noteest un texte libre pour nos analystes, de 1000 caractères au plus. N'y mettez jamais de noms, de numéros de documents ni d'autres données personnelles.- Un nouveau signalement répond 201. Le même signalement envoyé à nouveau renvoie celui déjà enregistré, avec 200. Envoyez une
Idempotency-Keypour réessayer sans risque. 20 signalements au plus par session. - Le même signalement est à un clic, sur la page de la session dans le tableau de bord.
POST /v1/partner/sessions/ses_.../fraud-reports
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: chargeback-8812
{
"status": "confirmed_fraud",
"category": "impostor",
"note": "Chargeback on the first deposit"
}
201 Created
{
"id": "flb_...",
"sessionId": "ses_...",
"status": "confirmed_fraud",
"category": "impostor",
"noteKept": true,
"createdAt": 1767225600000
}Suppression
DELETE /v1/partner/users/:externalUserId supprime les données d'un utilisateur quand il ferme son compte ou demande la suppression. La réponse indique ce qui a été supprimé maintenant, et ce qui est gardé, pourquoi et jusqu'à quand.
DELETE /v1/partner/users/user_42
Authorization: Bearer <partner key>
200 OK
{
"deleted": ["images", "chip_files", "video", "personal_data", "address"],
"retained": [
{ "category": "dedup_keys", "reason": "fraud_prevention", "until": 1924992000000 },
{ "category": "sanctions_screening_records", "reason": "legal_obligation", "until": 2082758400000 }
],
"backupsPurgedWithinDays": 30
}Erreurs
Chaque erreur a la même forme et un statut HTTP fixe par code.
{
"error": {
"code": "step_not_allowed",
"message": "This step is not allowed now.",
"requestId": "req_..."
}
}| Code | HTTP | Sens |
|---|---|---|
bad_request | 400 | La requête ne respecte pas le contrat. |
unauthorized | 401 | Clé ou jeton manquant ou incorrect. |
token_expired | 401 | Le jeton client a expiré. Créez-en un nouveau depuis votre backend. |
forbidden | 403 | Cette clé n'a pas le droit de faire cela. |
not_found | 404 | Cette session ou cet utilisateur n'existe pas. |
session_closed | 409 | La session est finale. Plus aucun envoi. |
step_not_allowed | 409 | Mauvais ordre, ou l'étape n'est pas nécessaire maintenant. |
revision_conflict | 409 | La révision de la session a changé. Relisez-la. |
idempotency_conflict | 409 | La même Idempotency-Key a servi pour une autre requête. |
challenge_expired | 410 | Le défi a expiré ou a déjà servi. Demandez-en un nouveau. |
payload_too_large | 413 | L'envoi est trop gros. |
unsupported_media_type | 415 | Ce type de fichier n'est pas accepté. |
rate_limited | 429 | Trop de requêtes. Réessayez plus tard. |
internal | 500 | Un problème est survenu de notre côté. |
dependency_unavailable | 503 | Un service dont nous avons besoin est en panne. Réessayez plus tard. |
Codes de motif
Les sessions portent des reasonCodes quand il faut renvoyer quelque chose ou qu'elles sont refusées. Affichez votre propre message traduit pour chaque code. Ne montrez jamais le code brut aux utilisateurs.
consent_missingconsent_withdrawnattestation_faileddocument_expireddocument_unreadabledocument_not_supporteddocument_suspected_tamperingdocument_number_visiblechip_verification_failedchip_proof_missingchip_data_mismatchmrz_invalidphoto_qualityface_not_foundface_mismatchliveness_failedunderagelocation_not_supportedresidence_not_supportedsanctions_matchpep_reviewduplicate_identitypossible_duplicatedeclaration_requires_reviewuser_requested_reviewreviewer_otheraddress_unreadableaddress_document_too_oldaddress_document_not_acceptedaddress_name_mismatchaddress_signature_requiredfunds_document_unreadablefunds_document_too_oldfunds_document_not_acceptedfunds_more_evidencefunds_evidence_rejected