Événements webhook
Référence complète de chaque type d'événement webhook de Certivant — schéma de l'enveloppe, structures de charge utile par événement, modèle d'idempotence et règles d'ordre.
Chaque charge utile de webhook partage la même enveloppe externe. Seul le bloc data change selon le type d'événement.
{
"event": "kyc.request.approved",
"requestId": "req_01J...",
"organizationId": "org_01H...",
"externalRef": "user-9281",
"subTenantName": "Acme Corp",
"subTenantExternalId": "cust_8842",
"status": "APPROVED",
"timestamp": "2026-05-27T10:04:21.000Z",
"data": { /* event-specific */ }
}
| Champ | Type | Remarques |
|---|---|---|
event | chaîne | Le type d'événement. Commence toujours par kyc. |
requestId | chaîne | L'identifiant de demande Certivant |
organizationId | chaîne | L'organisation propriétaire de la demande |
externalRef | chaîne | null | La clé étrangère que vous avez définie à la création |
subTenantName | chaîne | null | Mode partenaire : le nom d'affichage du client final défini sur la demande, le cas échéant |
subTenantExternalId | chaîne | null | Mode partenaire : votre identifiant de client final défini sur la demande — permet d'acheminer l'événement vers le bon client sans recherche |
status | énumération | Statut actuel de la demande au moment de l'événement |
timestamp | chaîne (ISO-8601) | Moment où l'événement a été mis en file d'attente |
data | objet | null | Charge utile propre à l'événement (voir ci-dessous) |
Catalogue des événements
kyc.webhook.test
Un test de connectivité — envoyé lorsque vous cliquez sur Tester pour une URL de webhook (pour tout le locataire, sous Paramètres KYC → Développeur, ou par formulaire dans l'éditeur de formulaire). Il ne s'agit pas d'un véritable événement de vérification : il ne contient pas de requestId et ne modifie rien de votre côté. Son seul objectif est de confirmer que votre point de terminaison est accessible.
Il comporte un indicateur explicite test: true au premier niveau, afin que votre gestionnaire puisse court-circuiter le traitement et répondre 2xx immédiatement — sans rechercher de demande, sans vérifier le status, et même sans vérifier la signature (il n'y a rien à protéger, car un ping ne modifie aucun état).
{
"event": "kyc.webhook.test",
"test": true,
"organizationId": "org_...",
"formId": "form_...", // present only for the per-form test
"formName": "KYC — Identity Verification",
"timestamp": "...",
"data": { "message": "This is a test webhook from your KYC platform." }
}
Gestionnaire recommandé :
const payload = JSON.parse(rawBody);
// Reachability ping — ack immediately, no auth needed.
if (payload.test === true || payload.event === "kyc.webhook.test") {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
// …verify the signature and process real events below…
Les véritables événements de résultat (dont le status figure dans l'énumération du cycle de vie) doivent tout de même faire l'objet d'une vérification de signature avant que vous n'agissiez : voir Vérification de la signature. L'événement de test est lui aussi signé (lorsqu'un secret est défini) — avec le même en-tête X-Certivant-Signature-V2 que les événements réels, et un horodatage en secondes Unix — vous pouvez donc le vérifier, sans y être obligé.
Le bouton Envoyer un événement de test du tableau de bord peut également envoyer une charge utile exemple de kyc.request.created, kyc.request.approved, kyc.request.rejected ou kyc.request.escalated. Les exemples ont la forme réelle de l'enveloppe, portent "test": true, utilisent un faux requestId (req_sample_…) et data.sample: true, et ne sont jamais enregistrés comme de véritables événements. Ne les traitez pas dans les chemins de code de production : testez d'abord payload.test === true.
kyc.request.created
Déclenché immédiatement après le retour 201 de POST /api/v1/kyc/requests. Utile si vous voulez confirmer la réception sans attendre que le demandeur soumette.
{
"event": "kyc.request.created",
"requestId": "req_...",
"organizationId": "org_...",
"externalRef": "user-9281",
"status": "PENDING_UPLOAD",
"timestamp": "...",
"data": null
}
kyc.request.processing
Déclenché dès que la demande passe à PROCESSING — c'est-à-dire lorsque le demandeur (via une session hébergée ou une soumission de formulaire) ou vous-même (via POST /requests/:id/submit) l'avez soumise. C'est le signal « soumis » : il arrive avant toute décision, de sorte que votre application peut faire passer l'utilisateur à un état « en cours d'examen » dès qu'il a terminé ses envois.
{
"event": "kyc.request.processing",
"requestId": "req_...",
"externalRef": "user-9281",
"status": "PROCESSING",
"timestamp": "...",
"data": null
}
Réagissez à cet événement lorsque vous voulez afficher la progression (« Votre vérification est en cours d'examen ») entre la soumission et l'événement final approved / rejected / escalated. Il contient externalRef et les champs de sous-locataire, ce qui vous permet de l'acheminer vers le bon utilisateur sans recherche.
kyc.document.uploaded
Déclenché à chaque fois qu'un document est rattaché à une demande. Utile si votre application affiche en temps réel un indicateur « documents reçus jusqu'ici ».
{
"event": "kyc.document.uploaded",
"requestId": "req_...",
"status": "PENDING_UPLOAD",
"data": {
"documentId": "doc_...",
"documentType": "GOVERNMENT_ID",
"side": "front",
"mimeType": "image/jpeg",
"fileSize": 412938
}
}
kyc.check.completed
Déclenché à la fin de chaque contrôle de vérification. Une seule demande peut produire plusieurs de ces événements — un pour l'authenticité du document, un pour la correspondance faciale, un pour la vivacité, etc.
{
"event": "kyc.check.completed",
"requestId": "req_...",
"status": "PROCESSING",
"data": {
"checkType": "face_match",
"result": "pass",
"score": 0.91,
"details": { "method": "deep_face_match" }
}
}
Valeurs possibles de checkType : document_authenticity, ocr, face_match, liveness, sanctions_screening, pep_screening, adverse_media, proof_of_address, phone_verification, email_verification, registry_lookup (KYB).
kyc.request.approved
L'événement majeur : la demande est approuvée. Il est déclenché aussi bien lorsque le moteur automatisé l'accepte que lorsqu'un réviseur humain approuve manuellement une demande escaladée (ou précédemment rejetée automatiquement) depuis le tableau de bord ou l'API /decision. Pour les approbations manuelles, data.decision vaut "approve" et data.reason contient la note du réviseur ; les champs détaillés du moteur (score, factors, …) ne sont présents que pour les décisions automatisées.
{
"event": "kyc.request.approved",
"requestId": "req_...",
"status": "APPROVED",
"data": {
"score": 94,
"decision": "approve",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.96 },
{ "name": "face_match", "weight": 0.3, "score": 0.91 },
{ "name": "liveness", "weight": 0.2, "score": 0.98 },
{ "name": "sanctions_screening", "weight": 0.2, "score": 1.0 }
],
"document": {
"type": "GOVERNMENT_ID",
"issuingCountry": "GB",
"expiryDate": "2031-04-12",
"isExpired": false
},
"extracted": {
"fullName": "Alex Singh",
"dateOfBirth": "1988-11-03",
"documentNumber": "GBR-1234567",
"address": "1 Example Street, London",
"nationality": "GB"
},
"liveness": { "isLive": true, "confidence": 0.98 },
"faceMatch": { "isMatch": true, "confidence": 0.91 },
"sanctions": { "isMatch": false, "topScore": 0, "listsChecked": ["OFAC", "UN", "EU", "UK_HMT"] }
}
}
Le bloc data.extracted est une mine d'or : enregistrez ces valeurs dans la fiche de l'utilisateur afin de ne pas redemander au demandeur les mêmes données deux fois.
kyc.request.rejected
La demande est rejetée — soit le moteur automatisé l'a rejetée (consultez data.disqualifiers et data.factors pour en connaître les motifs), soit un réviseur humain a rejeté manuellement une demande escaladée (data.decision = "reject", data.reason = la note du réviseur).
{
"event": "kyc.request.rejected",
"requestId": "req_...",
"status": "REJECTED",
"data": {
"score": 18,
"decision": "reject",
"disqualifiers": ["document_expired", "liveness_failed"],
"reviewTriggers": [],
"reason": "Document expired (2024-01-01) and liveness confidence below threshold (0.22).",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.0, "verdict": "fail" },
{ "name": "liveness", "weight": 0.2, "score": 0.22, "verdict": "fail" }
],
"document": {
"type": "GOVERNMENT_ID",
"expiryDate": "2024-01-01",
"isExpired": true
},
"liveness": { "isLive": false, "confidence": 0.22 }
}
}
Vous pouvez soumettre à nouveau la demande via POST /requests/:id/resubmit si votre politique le permet.
kyc.request.escalated
Le moteur n'était pas assez sûr de lui : scores limites, correspondance partielle avec les sanctions, document de mauvaise qualité, ou tout cas ayant dépassé le seuil d'escalade de votre organisation. Une personne de votre organisation doit examiner le dossier.
{
"event": "kyc.request.escalated",
"requestId": "req_...",
"status": "ESCALATED",
"data": {
"score": 62,
"decision": "escalate",
"reviewTriggers": ["sanctions_partial_match", "low_face_match_confidence"],
"factors": [...],
"sanctions": {
"isMatch": true,
"topScore": 0.72,
"listsChecked": ["OFAC", "UN", "EU", "UK_HMT"]
},
"faceMatch": { "isMatch": true, "confidence": 0.68 }
}
}
Un réviseur ouvre la demande dans le tableau de bord et décide. Lorsqu'il le fait, vous recevez un second webhook — kyc.request.approved ou kyc.request.rejected — avec la décision humaine consignée dans le journal d'audit.
kyc.request.cancelled
Déclenché lorsqu'une demande est explicitement annulée (actuellement uniquement depuis le tableau de bord — aucun point de terminaison API).
{
"event": "kyc.request.cancelled",
"requestId": "req_...",
"status": "CANCELLED",
"data": { "reason": "applicant_withdrew", "cancelledBy": "user_..." }
}
Événements de vérification en direct — conference.*
Les événements de la vérification Live ID (session vidéo en direct animée par un humain) utilisent leur propre
corps au format de conférence — conferenceId, kycRequestId, applicantId, organizationId, votre externalRef,
le status de la conférence et les horodatages — au lieu de l'enveloppe kyc.request.* ci-dessus. Ils sont déclenchés
aussi bien pour les conférences planifiées depuis le tableau de bord que pour celles planifiées par l'API.
| Événement | Déclenché lorsque |
|---|---|
conference.scheduled | Une vérification en direct est créée. |
conference.rescheduled | Elle est déplacée à une nouvelle heure (previousScheduledFor). |
conference.cancelled | Elle est annulée — par l'API, ou parce que le demandeur a refusé le consentement (cancellation). |
conference.started | L'animateur démarre la session (une fois par démarrage). |
conference.completed | L'animateur enregistre la décision (decision.decision, decision.outcome, notes, indicateur d'enregistrement, certificat). |
{
"event": "conference.completed",
"conferenceId": "cmf...",
"kycRequestId": "req_...",
"applicantId": "cma...",
"organizationId": "org_...",
"externalRef": "order-10042",
"status": "completed",
"decision": { "decision": "verified", "outcome": "approved", "requestStatus": "APPROVED", "notes": "..." },
"timestamp": "..."
}
Charge utile complète et API de planification : API de vérification en direct.
Idempotence
Certivant peut livrer le même événement plus d'une fois (coupures réseau, nouvelles tentatives — voir Nouvelles tentatives). Rendez votre gestionnaire idempotent.
L'en-tête X-Certivant-Event-Id (également envoyé sous le nom X-KYC-Event-Id) est unique pour chaque événement. Modèle courant :
async function handleWebhook(rawBody: string, headers: Headers) {
const eventId = headers.get("X-KYC-Event-Id")!;
// Skip if already processed
const seen = await db.webhookEvent.findUnique({ where: { id: eventId } });
if (seen) return;
const payload = JSON.parse(rawBody);
// Do the work + record the event in a single transaction
await db.$transaction([
db.webhookEvent.create({ data: { id: eventId, eventType: payload.event } }),
...yourBusinessLogic(payload),
]);
}
X-KYC-Event-Id est stable d'une tentative à l'autre : le même événement logique conserve le même identifiant, quel que soit le nombre de tentatives de livraison.
Ordre des événements
Les événements concernant des demandes différentes peuvent arriver dans n'importe quel ordre. Les événements concernant la même demande sont mis en file d'attente dans l'ordre de leur génération, mais les nouvelles tentatives peuvent les entremêler : par exemple, un événement processing bloqué peut arriver après un événement approved pour la même demande.
Modèle défensif : utilisez toujours le champ status comme source de vérité, et ignorez les événements dont le status est « plus ancien » que celui que vous avez déjà enregistré. Le cycle de vie n'avance que dans un sens (PENDING_UPLOAD → PROCESSING → APPROVED/REJECTED/ESCALATED) : si votre base indique que la demande est APPROVED et qu'un événement kyc.request.processing tardif arrive, éliminez-le.
Tester les gestionnaires d'événements en local
Deux boutons du tableau de bord vous aident ici :
- Tester (à côté de n'importe quelle URL de webhook, pour tout le locataire ou par formulaire) envoie un ping de connectivité
kyc.webhook.test(test: true). Utilisez-le pour confirmer que votre point de terminaison est accessible et renvoie2xx— il n'a besoin d'aucune donnée de demande ni d'aucun secret configuré. - Envoyer un événement de test envoie une charge utile complète
kyc.request.approvedavec des données d'exemple, afin que vous puissiez exercer votre véritable chemin de traitement des résultats.
Pour tester d'autres types d'événements, écrivez un petit script qui construit la même enveloppe et l'envoie en POST vers votre URL de développement via un tunnel :
# tunnel localhost:3000 to a public URL
ngrok http 3000
# Then in the dashboard webhook settings, paste the ngrok URL,
# trigger real test requests via the API, and watch your logs.
Vérification de la signature
Vérifiez l'en-tête X-Certivant-Signature-V2 (HMAC-SHA256 sur timestamp.body, tolérance de 300 s) pour que vous puissiez faire confiance aux charges utiles des webhooks. Inclut des exemples en Node, Python, Go, Ruby et PHP.
Nouvelles tentatives et livraison
Comment Certivant renouvelle les livraisons de webhooks en échec — calendrier exact d'espacement, règle de délai d'attente de 10 secondes, reprise des événements épuisés, et modèle de gestionnaire rapide pour rester dans le budget.