Cuando una sesión se resuelve, kyc.ar hace un POST a los endpoints que
registraste, con el evento firmado en el cuerpo.
El sobre del evento
{
"id": "evt_7QK3M9ZP2X8V4T0R6B1D5F3H7J",
"object": "event",
"api_version": "v1",
"type": "verification_session.approved",
"created_at": "2026-08-19T14:22:47.903Z",
"data": {
"object": { "...": "la sesión" }
}
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string | siempre | Id del evento, evt_…. Es determinístico: el mismo evento lógico lleva el mismo id en cada reintento y en todos tus endpoints. Es la clave de deduplicación. |
object | string | siempre | Constante "event". |
api_version | string | siempre | Versión del formato del payload. Hoy siempre "v1". |
type | string | siempre | Tipo de evento. Ver la tabla de abajo. |
created_at | string | siempre | ISO 8601 UTC del momento del envío (no de la resolución de la sesión). |
data.object | object | siempre | La sesión de verificación. |
Headers de cada entrega
| Campo | Tipo | Ejemplo | Descripción |
|---|---|---|---|
X-KycAr-Signature | Firma | t=1787234567,v1=6a1f… | HMAC-SHA256 del cuerpo. Validalo siempre: ver Verificar la firma. |
X-KycAr-Event-Id | Id | evt_7QK3M9ZP… | El mismo id que viaja en el cuerpo. Deduplicá por este valor. |
X-KycAr-Event-Type | Tipo | verification_session.approved | El tipo, también en header, para rutear sin parsear el cuerpo. |
Content-Type | MIME | application/json; charset=utf-8 | El cuerpo siempre es JSON. |
User-Agent | Cliente | kycar-webhooks/1.0 | Identifica al emisor. |
Tipos de evento
| Campo | Tipo | Cuándo se dispara | Descripción |
|---|---|---|---|
verification_session.approved | Terminal | La sesión se aprueba | La persona completó el flujo y todos los validadores que cuentan dieron bien. |
verification_session.rejected | Terminal | La sesión se rechaza | Al menos un validador falló o agotó sus reintentos. Los motivos están en result.reason_codes. |
verification_session.completed | Terminal | Junto a approved o rejected | Catch-all para quien sólo necesita saber “hay veredicto”. Se emite además del evento específico. No se emite por vencimiento, abandono ni cancelación. |
verification_session.expired | Terminal | Vencimiento o abandono | Cubre las dos formas de cierre por tiempo. Para distinguirlas mirá result.outcome: "expired" si venció el link, "abandoned" si la persona empezó y dejó el flujo inactivo. |
verification_session.canceled | Terminal | Cancelaste la sesión | Se emite al usar POST /v1/verification-sessions/{id}/cancel. |
verification_session.review_required | No terminal | Todavía no se emite | Declarado y suscribible, pero hoy ningún emisor lo publica. Ver la nota de abajo. |
verification_session.created | No terminal | Todavía no se emite | Declarado y suscribible, pero hoy ningún emisor lo publica. Ver la nota de abajo. |
La sesión dentro del evento
Estos campos viajan siempre:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string | siempre | La sesión, vs_…. |
object | string | siempre | "verification_session" |
status | enum | siempre | Estado interno de la sesión. |
mode | enum | siempre | "live" o "test". |
project_id | string | siempre | prj_… |
config_version | string | siempre | Versión congelada del flujo con la que corrió la sesión. |
created_at | string | siempre | Creación de la sesión. |
external_ref | string | si se envió | Tu referencia de correlación. |
result | object | si hay resolución | Objeto con outcome, reason_codes y steps. |
completed_at | string | si es terminal | Momento en que quedó en estado terminal. |
Sin opt-in, el payload no lleva ningún dato personal: sólo ids, veredicto y motivos.
Campos opt-in por proyecto
Cada proyecto decide, en su configuración, qué datos adicionales viajan. La decisión se toma sobre la configuración congelada de esa sesión: cambiar el proyecto no altera lo que ya se envió.
| Campo | Tipo | Opción del proyecto | Descripción |
|---|---|---|---|
result.document | object | documentData | Datos extraídos del documento: tipo, serie, número, apellido, nombres, sexo, fecha de nacimiento, CUIL y vencimiento. Nunca incluye scores internos ni las claves de las imágenes. |
result.verified_channels | object | verifiedChannels | Email y teléfono que pasaron OTP. Ver la limitación de abajo. |
result.form_data | object | formData | Respuestas del paso de formulario, como diccionario de strings. |
evidence_links | array | evidenceLinks | Links firmados a las imágenes capturadas, generados al momento del envío y con 5 minutos de vida. Un item por artefacto — front, back y front_angle del documento — con el último intento de captura de cada uno. |
Un evento con todo activado:
{
"id": "evt_7QK3M9ZP2X8V4T0R6B1D5F3H7J",
"object": "event",
"api_version": "v1",
"type": "verification_session.approved",
"created_at": "2026-08-19T14:22:47.903Z",
"data": {
"object": {
"id": "vs_01K2M4P6R8T0V2X4Z6B8D0F2H4",
"object": "verification_session",
"status": "approved",
"mode": "live",
"project_id": "prj_01J9Z2K3M4N5P6Q7R8S9T0V1W2",
"config_version": "cfg_00000003",
"created_at": "2026-08-19T14:03:11.482Z",
"external_ref": "cliente-4821",
"result": {
"outcome": "approved",
"reason_codes": [],
"steps": [
{ "id": "consent", "type": "consent", "verdict": "approved" },
{ "id": "documento", "type": "document_ar", "verdict": "approved" },
{ "id": "selfie", "type": "face_liveness", "verdict": "approved" }
],
"document": {
"doc_type": "dni_card",
"series": "00123456789",
"number": "34567890",
"surname": "GONZALEZ",
"given_names": "MARIA LAURA",
"sex": "F",
"birth_date": "1989-06-14",
"cuil": "27345678901",
"expiry_date": "2032-06-14"
},
"form_data": { "ocupacion": "Contadora" }
},
"completed_at": "2026-08-19T14:22:46.551Z",
"evidence_links": [
{
"artifact": "front",
"url": "https://kycar-evidence-....s3.amazonaws.com/...&X-Amz-Signature=...",
"expires_at": "2026-08-19T14:27:47.903Z"
},
{
"artifact": "back",
"url": "https://kycar-evidence-....s3.amazonaws.com/...&X-Amz-Signature=...",
"expires_at": "2026-08-19T14:27:47.903Z"
}
]
}
}
}
Las dos capas de suscripción
Para que un evento llegue tienen que coincidir las dos:
- El proyecto declara qué eventos dispara, en su configuración
(
webhookEvents). Un proyecto sin eventos marcados no dispara ninguno. - El endpoint declara cuáles recibe, en su campo
events.
Si registraste el endpoint y no te llega nada, revisá primero la configuración del proyecto: es la causa más frecuente.
Qué tiene que hacer tu receptor
| Campo | Tipo | Regla | Descripción |
|---|---|---|---|
Responder 2xx | Obligatorio | dentro de 10 s | Cualquier otra cosa —incluido un 3xx, porque no seguimos redirecciones— cuenta como fallo y dispara el reintento. |
Responder rápido | Obligatorio | antes del timeout | Validá la firma, encolá el trabajo y respondé. No proceses el evento de forma sincrónica dentro del request. |
Deduplicar | Obligatorio | por X-KycAr-Event-Id | La entrega es at-least-once: podés recibir el mismo evento más de una vez, también en endpoints que ya respondieron OK. |
Validar la firma | Obligatorio | sobre el cuerpo crudo | Sin esto, cualquiera que conozca tu URL puede fabricar una aprobación. |
Tolerar campos nuevos | Recomendado | no romper con extras | El payload v1 es estable, pero agregar un campo opcional no se considera cambio incompatible. |