Webhooks

Eventos y payload

Tipos de evento, el payload v1 y qué campos son opt-in por proyecto.

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" }
  }
}
CampoTipoRequeridoDescripción
idstringsiempreId 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.
objectstringsiempreConstante "event".
api_versionstringsiempreVersión del formato del payload. Hoy siempre "v1".
typestringsiempreTipo de evento. Ver la tabla de abajo.
created_atstringsiempreISO 8601 UTC del momento del envío (no de la resolución de la sesión).
data.objectobjectsiempreLa sesión de verificación.

Headers de cada entrega

CampoTipoEjemploDescripción
X-KycAr-SignatureFirmat=1787234567,v1=6a1f…HMAC-SHA256 del cuerpo. Validalo siempre: ver Verificar la firma.
X-KycAr-Event-IdIdevt_7QK3M9ZP…El mismo id que viaja en el cuerpo. Deduplicá por este valor.
X-KycAr-Event-TypeTipoverification_session.approvedEl tipo, también en header, para rutear sin parsear el cuerpo.
Content-TypeMIMEapplication/json; charset=utf-8El cuerpo siempre es JSON.
User-AgentClientekycar-webhooks/1.0Identifica al emisor.

Tipos de evento

CampoTipoCuándo se disparaDescripción
verification_session.approvedTerminalLa sesión se apruebaLa persona completó el flujo y todos los validadores que cuentan dieron bien.
verification_session.rejectedTerminalLa sesión se rechazaAl menos un validador falló o agotó sus reintentos. Los motivos están en result.reason_codes.
verification_session.completedTerminalJunto a approved o rejectedCatch-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.expiredTerminalVencimiento o abandonoCubre 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.canceledTerminalCancelaste la sesiónSe emite al usar POST /v1/verification-sessions/{id}/cancel.
verification_session.review_requiredNo terminalTodavía no se emiteDeclarado y suscribible, pero hoy ningún emisor lo publica. Ver la nota de abajo.
verification_session.createdNo terminalTodavía no se emiteDeclarado y suscribible, pero hoy ningún emisor lo publica. Ver la nota de abajo.

La sesión dentro del evento

Estos campos viajan siempre:

CampoTipoRequeridoDescripción
idstringsiempreLa sesión, vs_….
objectstringsiempre"verification_session"
statusenumsiempreEstado interno de la sesión.
modeenumsiempre"live" o "test".
project_idstringsiempreprj_…
config_versionstringsiempreVersión congelada del flujo con la que corrió la sesión.
created_atstringsiempreCreación de la sesión.
external_refstringsi se envióTu referencia de correlación.
resultobjectsi hay resoluciónObjeto con outcome, reason_codes y steps.
completed_atstringsi es terminalMomento 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ó.

CampoTipoOpción del proyectoDescripción
result.documentobjectdocumentDataDatos 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_channelsobjectverifiedChannelsEmail y teléfono que pasaron OTP. Ver la limitación de abajo.
result.form_dataobjectformDataRespuestas del paso de formulario, como diccionario de strings.
evidence_linksarrayevidenceLinksLinks 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:

  1. El proyecto declara qué eventos dispara, en su configuración (webhookEvents). Un proyecto sin eventos marcados no dispara ninguno.
  2. 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

CampoTipoReglaDescripción
Responder 2xxObligatoriodentro de 10 sCualquier otra cosa —incluido un 3xx, porque no seguimos redirecciones— cuenta como fallo y dispara el reintento.
Responder rápidoObligatorioantes del timeoutValidá la firma, encolá el trabajo y respondé. No proceses el evento de forma sincrónica dentro del request.
DeduplicarObligatoriopor X-KycAr-Event-IdLa 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 firmaObligatoriosobre el cuerpo crudoSin esto, cualquiera que conozca tu URL puede fabricar una aprobación.
Tolerar campos nuevosRecomendadono romper con extrasEl payload v1 es estable, pero agregar un campo opcional no se considera cambio incompatible.