Eventi webhook

Riferimento completo di ogni tipo di evento webhook di Certivant — schema della busta, forme dei payload per evento, schema di idempotenza e regole di ordinamento.

Ogni payload webhook condivide la stessa busta esterna. Cambia solo il blocco data, per tipo di evento.

{
  "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 */ }
}
CampoTipoNote
eventstringaIl tipo di evento. Inizia sempre con kyc.
requestIdstringaL'ID della richiesta Certivant
organizationIdstringaL'organizzazione proprietaria della richiesta
externalRefstringa | nullLa chiave esterna impostata alla creazione
subTenantNamestringa | nullModalità partner: il nome visualizzato del cliente finale impostato sulla richiesta, se presente
subTenantExternalIdstringa | nullModalità partner: l'ID del Suo cliente finale impostato sulla richiesta — instradi l'evento al cliente giusto senza una ricerca
statusenumStato attuale della richiesta al momento dell'evento
timestampstringa (ISO-8601)Quando l'evento è stato accodato
dataoggetto | nullPayload specifico dell'evento (vedi sotto)

Catalogo degli eventi

kyc.webhook.test

Un controllo di connettività — inviato quando preme Test su un URL webhook (a livello di tenant in Impostazioni KYC → Sviluppatore, oppure per modulo nell'editor dei moduli). Non è un vero evento di verifica: non contiene un requestId e non modifica nulla dalla Sua parte. Il suo unico scopo è confermare che il Suo endpoint sia raggiungibile.

Include un flag esplicito di primo livello test: true, così il Suo gestore può chiudere subito e rispondere 2xx — senza cercare una richiesta, controllare lo status, né verificare la firma (non c'è nulla da proteggere, dato che un ping non modifica alcuno stato).

{
  "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." }
}

Gestore consigliato:

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…

Gli eventi di risultato reali (con uno status dell'enum del ciclo di vita) devono comunque essere verificati con la firma prima di agire — vedi Verifica della firma. Anche l'evento di prova è firmato (quando è impostato un segreto), con la stessa intestazione X-Certivant-Signature-V2 degli eventi reali e un timestamp in secondi Unix: quindi può verificarlo, ma non è obbligatorio.

Il pulsante Invia un evento di prova della dashboard può anche inviare un payload di esempio kyc.request.created, kyc.request.approved, kyc.request.rejected o kyc.request.escalated. Gli esempi hanno la forma reale della busta, contengono "test": true, usano un requestId fittizio (req_sample_…) e data.sample: true, e non vengono mai memorizzati come eventi reali. Non agisca su di essi nei percorsi di codice di produzione: si ramifichi prima su payload.test === true.

kyc.request.created

Si attiva subito dopo che POST /api/v1/kyc/requests restituisce 201. Utile se vuole confermare la ricezione senza attendere che il richiedente invii.

{
  "event": "kyc.request.created",
  "requestId": "req_...",
  "organizationId": "org_...",
  "externalRef": "user-9281",
  "status": "PENDING_UPLOAD",
  "timestamp": "...",
  "data": null
}

kyc.request.processing

Si attiva nel momento in cui la richiesta passa in PROCESSING — cioè quando il richiedente (tramite una sessione ospitata o un invio di modulo) oppure Lei (tramite POST /requests/:id/submit) l'ha inviata. È il segnale di «inviata»: arriva prima di qualsiasi decisione, quindi la Sua app può mostrare l'utente in stato «in revisione» non appena ha finito di caricare.

{
  "event": "kyc.request.processing",
  "requestId": "req_...",
  "externalRef": "user-9281",
  "status": "PROCESSING",
  "timestamp": "...",
  "data": null
}

Agisca su di esso quando vuole mostrare l'avanzamento («La Sua verifica è in corso di revisione») tra l'invio e l'evento finale approved / rejected / escalated. Trasporta externalRef e i campi del sub-tenant, così può instradarlo all'utente giusto senza una ricerca.


kyc.document.uploaded

Si attiva ogni volta che un documento viene collegato a una richiesta. Utile se la Sua app mostra un indicatore in tempo reale dei «documenti ricevuti finora».

{
  "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

Si attiva al termine di ciascun controllo di verifica. Una singola richiesta può produrne più di uno — uno per l'autenticità del documento, uno per il confronto del volto, uno per la vivacità, ecc.

{
  "event": "kyc.check.completed",
  "requestId": "req_...",
  "status": "PROCESSING",
  "data": {
    "checkType": "face_match",
    "result": "pass",
    "score": 0.91,
    "details": { "method": "deep_face_match" }
  }
}

Possibili valori di 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

Il caso principale — la richiesta è approvata. Si attiva sia quando il motore automatico la approva, sia quando un revisore umano approva manualmente una richiesta inoltrata (o precedentemente rifiutata automaticamente) dalla dashboard o dall'API /decision. Per le approvazioni manuali data.decision è "approve" e data.reason riporta la nota del revisore; i campi ricchi del motore (score, factors, …) sono presenti solo sulle decisioni automatiche.

{
  "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"] }
  }
}

Il blocco data.extracted è la Sua miniera d'oro: salvi quei valori nel record dell'utente, così da non chiedere di nuovo al richiedente gli stessi dati.


kyc.request.rejected

La richiesta è rifiutata — o il motore automatico l'ha rifiutata (guardi data.disqualifiers e data.factors per i motivi), oppure un revisore umano ha rifiutato manualmente una richiesta inoltrata (data.decision = "reject", data.reason = la nota del revisore).

{
  "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 }
  }
}

Può reinviare la richiesta tramite POST /requests/:id/resubmit, se la Sua policy lo consente.


kyc.request.escalated

Il motore non era sicuro — punteggi al limite, corrispondenza parziale nelle sanzioni, documento di bassa qualità, oppure qualsiasi cosa che abbia superato la soglia di inoltro della Sua organizzazione. Una persona della Sua organizzazione deve esaminarla.

{
  "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 revisore apre la richiesta nella dashboard e decide. Quando lo fa, riceve un secondo webhook — kyc.request.approved oppure kyc.request.rejected — con la decisione umana annotata nel registro di audit.


kyc.request.cancelled

Si attiva quando una richiesta viene annullata esplicitamente (attualmente solo dalla dashboard — nessun endpoint API).

{
  "event": "kyc.request.cancelled",
  "requestId": "req_...",
  "status": "CANCELLED",
  "data": { "reason": "applicant_withdrew", "cancelledBy": "user_..." }
}

Eventi di verifica dal vivo — conference.*

Gli eventi della verifica dell'identità dal vivo Live ID (sessione video in diretta condotta da un operatore) usano un proprio corpo con la forma della conferenza — conferenceId, kycRequestId, applicantId, organizationId, il Suo externalRef, lo status della conferenza e i timestamp — invece della busta kyc.request.* descritta sopra. Si attivano per le conferenze pianificate sia dalla dashboard sia dall'API.

EventoSi attiva quando
conference.scheduledViene creata una verifica dal vivo.
conference.rescheduledViene spostata a un nuovo orario (previousScheduledFor).
conference.cancelledViene annullata — tramite API, oppure perché il richiedente ha rifiutato il consenso (cancellation).
conference.startedIl conduttore avvia la sessione (una volta per avvio).
conference.completedIl conduttore registra la decisione (decision.decision, decision.outcome, note, indicatore di registrazione, certificato).
{
  "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": "..."
}

Payload completo e API di pianificazione: Verifica dal vivo.


Idempotenza

Certivant può consegnare lo stesso evento più di una volta (interruzioni di rete, nuovi tentativi — vedi Tentativi e consegna). Renda idempotente il Suo gestore.

L'intestazione X-Certivant-Event-Id (inviata anche come X-KYC-Event-Id) è univoca per evento. Schema comune:

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 è stabile tra i nuovi tentativi: lo stesso evento logico mantiene lo stesso ID indipendentemente da quanti tentativi di consegna avvengono.

Ordinamento

Gli eventi per richieste diverse possono arrivare in qualsiasi ordine. Gli eventi per la stessa richiesta vengono accodati nell'ordine in cui sono stati generati, ma i nuovi tentativi possono intercalarsi — per esempio, un evento processing bloccato potrebbe arrivare dopo un evento approved della stessa richiesta.

Schema difensivo: usi sempre il campo status come fonte di verità, e ignori gli eventi il cui status è «più vecchio» di quanto già registrato. Il ciclo di vita procede solo in avanti (PENDING_UPLOAD → PROCESSING → APPROVED/REJECTED/ESCALATED), quindi se il Suo database dice che la richiesta è APPROVED e arriva un evento kyc.request.processing in ritardo, lo scarti.

Test dei gestori di eventi in locale

Due pulsanti della dashboard aiutano:

  • Test (accanto a qualsiasi URL webhook, a livello di tenant o per modulo) invia un ping di connettività kyc.webhook.test (test: true). Lo usi per confermare che il Suo endpoint sia raggiungibile e restituisca 2xx — non richiede dati di richiesta né un segreto configurato.
  • Invia evento di prova invia un payload completo kyc.request.approved con dati di esempio, così può verificare il Suo vero percorso di gestione dell'esito.

Per testare altri tipi di evento, scriva un piccolo script che costruisca la stessa busta e la invii con POST al Suo URL di sviluppo tramite 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.