> For the complete documentation index, see [llms.txt](https://docs.facephi.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.facephi.com/productos/idv-suite/flujos-and-integraciones/configuracion-tecnica-del-cliente/webhook.md).

# Webhook

## Documentacion de Webhooks

### Objetivo

Los webhooks permiten que tu sistema reciba notificaciones en tiempo real a medida que avanza una operacion de verificacion:

* inicio de operacion,
* capturas de evidencia (documento, selfie, NFC),
* evaluaciones biometricas,
* resultado final.

Con esto puedes actualizar estados de negocio, disparar reglas y mostrar trazabilidad al usuario final sin consultar continuamente por polling.

### Como funciona el envio

1. Configuras una URL HTTPS de recepcion en tu integracion.
2. El sistema envia eventos por `POST` con payload JSON.
3. Solo se envian los eventos que hayas suscrito en tu configuracion.
4. Cada mensaje incluye firma para validar autenticidad.

Recomendacion para tu endpoint:

* responder rapido con `2xx` cuando el mensaje sea aceptado,
* procesar de forma asincrona cuando sea posible,
* manejar idempotencia usando `id` del evento.

### Seguridad y autenticidad

Cada webhook incluye:

* cabecera `Content-Type: application/json`,
* cabeceras de seguridad personalizadas (si las definiste),
* campo `signature` dentro del payload.

La firma se calcula con HMAC SHA-256 sobre el contenido del evento y una clave compartida de integracion. Debes validar esta firma antes de considerar el mensaje como confiable.

#### Como se calcula `signature`

1. Se toma el evento **sin** el campo `signature`.
2. Se serializa con [JSON Canonicalization Scheme (JCS, RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785).
3. Se aplica HMAC-SHA256 del JSON canónico.
4. El digest se codifica en **base64**.

El payload se entrega ya serializado en JCS (incluido `signature`), para que el body coincida con el contrato de firma.

#### Como verificarla

Pasos recomendados (robustos en cualquier lenguaje):

1. Parsear el JSON recibido.
2. Leer y guardar `signature`.
3. Eliminar `signature` del objeto.
4. Canonicalizar el objeto restante con JCS (RFC 8785).
5. Calcular `HMAC-SHA256` + base64 con la misma clave compartida de integracion.
6. Comparar en tiempo constante con la firma recibida.

Si consumes el body crudo tal cual, en runtimes que preservan el orden de keys al parsear/serializar suele bastar con quitar `signature` antes del HMAC. Aun asi, la verificación formal documentada es **JCS + HMAC-SHA256 + base64**.

### Estructura general del webhook

Todos los eventos siguen la misma estructura base. El body se entrega en orden JCS:

```json
{
	"data": {},
	"id": "uuid-del-evento",
	"signature": "firma-hmac-base64"
	"source": "/operations/{operationId}",
	"specversion": "1.0",
	"time": "2026-03-06T12:00:00.000Z",
	"type": "com.idv_suite.api.workflows.algo.v1"
}
```

Significado de campos:

* `id`: identificador unico del mensaje (usar para evitar duplicados).
* `type`: tipo de evento (define como interpretar `data`).
* `source`: operacion origen del evento.
* `time`: fecha/hora UTC de emision.
* `data`: contenido de negocio del evento.
* `signature`: firma del mensaje (HMAC-SHA256 en base64 sobre el evento canónico sin este campo).

### Base TypeScript

Todos los eventos comparten este sobre base:

```ts
type WebhookSource = `/operations/${string}`;

type WebhookEnvelope<TType extends string, TData> = {
	specversion: '1.0';
	id: string;
	type: TType;
	source: WebhookSource;
	time: string;
	data: TData;
	signature: string;
};
```

Notas de modelado:

* `specversion` hoy es siempre `"1.0"`.
* `id`, `time` y `signature` no tienen catalogo cerrado.
* `source` siempre sigue el patron `/operations/{operationId}`.
* En cada evento se cierran como enum solo los valores que la implementacion actual fija de forma explicita.
* Cuando un proveedor externo o una etapa interna no fija un catalogo cerrado, el campo debe dejarse como `string`, `Record<string, unknown>` o una estructura abierta equivalente.

### Eventos actualmente disponibles

#### 1) Operacion iniciada

* **Tipo**: `com.idv_suite.api.workflows.operation_started.v1`
* **Cuando se envia**: al crear/iniciar una operacion.
* **Para que sirve**: abrir seguimiento de la operacion y correlacionar con tus sistemas.

Tipado TypeScript:

```ts
type OperationStartedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.operation_started.v1',
	{
		sessionId: string;
		authenticationId?: string;
		customerId: string;
		source: string;
		document?: {
			type?: string;
			number?: string;
			issuer?: string;
			gender?: string;
			code?: string;
			name?: string;
			surname?: string;
		};
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "4f65587f-2c4f-4ea2-b7ff-8fbf7d6fe8e8",
	"type": "com.idv_suite.api.workflows.operation_started.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:00:00.000Z",
	"data": {
		"customerId": "user-123",
		"sessionId": "sess-789",
		"source": "sdk.mobile"
	},
	"signature": "firma-hmac-base64"
}
```

#### 2) Consentimiento de terminos

* **Tipo**: `com.idv_suite.api.workflows.terms_consent.v1`
* **Cuando se envia**: al aceptar o rechazar terminos.
* **Para que sirve**: trazabilidad legal y reglas de continuidad del flujo.

Tipado TypeScript:

```ts
type TermsConsentWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.terms_consent.v1',
	{
		stepId: string;
		success: boolean;
		timestamp: string;
		accepted: 'accepted' | 'rejected';
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "18a26cf2-4556-4f69-9be5-e0b677f4de82",
	"type": "com.idv_suite.api.workflows.terms_consent.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:00:15.000Z",
	"data": {
		"stepId": "1d80cdd1-9dfb-4ef3-b0aa-530f2db60a02",
		"success": true,
		"timestamp": "2026-03-06T12:00:14.000Z",
		"accepted": "accepted"
	},
	"signature": "firma-hmac-base64"
}
```

#### 3) Documento capturado (ID/OCR)

* **Tipo**: `com.idv_suite.api.workflows.id_captured.v1`
* **Cuando se envia**: al completar captura y extraccion de datos del documento.
* **Para que sirve**: poblar datos documentales y validar consistencia.

Tipado TypeScript:

```ts
type IdCapturedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.id_captured.v1',
	{
		stepId: string;
		success: boolean;
		data: Record<string, unknown>;
		summary: Record<string, unknown>;
		assets?: string[];
	}
>;
```

Notas:

* `data` y `summary` no tienen un schema cerrado en la implementacion actual.
* `assets` contiene ids de assets asociados al evento.

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "426133f2-50ea-4f4c-8fe8-5435286ea737",
	"type": "com.idv_suite.api.workflows.id_captured.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:01:50.000Z",
	"data": {
		"stepId": "7f4af246-d48c-4ce6-b3b1-bdf65ba7c9dd",
		"success": true,
		"data": {
			"documentNumber": "X1234567"
		},
		"summary": {
			"name": "JUAN",
			"surname": "MARTINEZ"
		},
		"assets": ["asset-portrait-id", "asset-signature-id"]
	},
	"signature": "firma-hmac-base64"
}
```

#### 4) Selfie capturada

* **Tipo**: `com.idv_suite.api.workflows.selfie_captured.v1`
* **Cuando se envia**: al completarse la captura facial.
* **Para que sirve**: marcar avance de etapa biometrica.

Tipado TypeScript:

```ts
type SelfieCapturedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.selfie_captured.v1',
	{
		stepId: string;
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "f0d3da62-0711-4b50-93f8-5034b4d4a4bb",
	"type": "com.idv_suite.api.workflows.selfie_captured.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:01:02.000Z",
	"data": {
		"stepId": "ab4b360f-1f6b-4ca4-ae44-8676f3705747"
	},
	"signature": "firma-hmac-base64"
}
```

#### 5) NFC capturado

* **Tipo**: `com.idv_suite.api.workflows.nfc_captured.v1`
* **Cuando se envia**: cuando hay lectura NFC del documento.
* **Para que sirve**: enriquecer y contrastar informacion documental.

Tipado TypeScript:

```ts
type NfcCapturedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.nfc_captured.v1',
	{
		stepId: string;
		success: boolean;
		data: Record<string, unknown>;
		summary: Record<string, unknown>;
		assets?: string[];
	}
>;
```

Notas:

* Igual que en `id_captured`, `data` y `summary` siguen siendo abiertos.

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "cb08900a-8ef2-4f61-95f1-0e39f7686cef",
	"type": "com.idv_suite.api.workflows.nfc_captured.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:01:30.000Z",
	"data": {
		"stepId": "17761cae-0150-43f7-a5b2-a15e7a59f659",
		"success": true,
		"data": {
			"documentNumber": "X1234567"
		},
		"summary": {
			"name": "JUAN",
			"surname": "MARTINEZ"
		}
	},
	"signature": "firma-hmac-base64"
}
```

#### 6) Liveness pasivo evaluado

* **Tipo**: `com.idv_suite.api.workflows.passive_liveness_evaluated.v1`
* **Cuando se envia**: tras evaluar prueba de vida pasiva.
* **Para que sirve**: detectar potenciales intentos de suplantacion.

Tipado TypeScript:

```ts
type PassiveLivenessEvaluatedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.passive_liveness_evaluated.v1',
	{
		stepId: string;
		success: boolean;
		diagnostic: string;
		assets?: string[];
	}
>;
```

Notas:

* `diagnostic` no tiene enum cerrado en codigo; el ejemplo conocido es `"Live"`.

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "1029c9ed-8945-46e6-9908-9a76ecf402f4",
	"type": "com.idv_suite.api.workflows.passive_liveness_evaluated.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:02:11.000Z",
	"data": {
		"stepId": "95b3f1f9-cf8f-469c-b698-414b3a9656e6",
		"success": true,
		"diagnostic": "Live",
		"assets": ["asset-selfie-id"]
	},
	"signature": "firma-hmac-base64"
}
```

#### 7) Deteccion de inyeccion

* **Tipo**: `com.idv_suite.api.workflows.injection_attack_detected.v1`
* **Cuando se envia**: tras evaluar riesgo de inyeccion/sintetico.
* **Para que sirve**: fortalecer controles antifraude en tiempo real.

Tipado TypeScript:

```ts
type InjectionAttackDetectedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.injection_attack_detected.v1',
	{
		stepId: string;
		success: boolean;
		score: number;
		probability: number;
		status: 'REAL' | 'SPOOF';
		assets?: string[];
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "0203b7e9-5ed4-4444-af6d-46cf15f8fc76",
	"type": "com.idv_suite.api.workflows.injection_attack_detected.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:02:30.000Z",
	"data": {
		"stepId": "abf8f66f-0b8d-4fce-a593-eb68d51c210f",
		"success": true,
		"score": 0.12,
		"probability": 0.06,
		"status": "REAL",
		"assets": ["asset-selfie-id"]
	},
	"signature": "firma-hmac-base64"
}
```

#### 8) Matching facial evaluado

* **Tipo**: `com.idv_suite.api.workflows.facial_authentication_evaluated.v1`
* **Cuando se envia**: al comparar rostro de documento vs selfie.
* **Para que sirve**: validar correspondencia biometrica.

Tipado TypeScript:

```ts
type FacialAuthenticationEvaluatedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.facial_authentication_evaluated.v1',
	{
		stepId: string;
		success: boolean;
		authStatus: string;
		similarity: number;
		assets?: string[];
	}
>;
```

Notas:

* `authStatus` no tiene enum cerrado en codigo. El ejemplo conocido es `"Positive"`.

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "0087cf6a-77f9-42f7-8f78-8ae94f32bd9a",
	"type": "com.idv_suite.api.workflows.facial_authentication_evaluated.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:02:49.000Z",
	"data": {
		"stepId": "6fe21ca0-2914-49ac-b87c-d3c665304be4",
		"success": true,
		"authStatus": "Positive",
		"similarity": 0.93,
		"assets": ["asset-id-portrait", "asset-selfie"]
	},
	"signature": "firma-hmac-base64"
}
```

#### 9) Enrollment facial

* **Tipo**: `com.idv_suite.api.workflows.facial_enrollment.v1`
* **Cuando se envia**: al registrar una identidad biometrica.
* **Para que sirve**: habilitar futuras autenticaciones 1:1.

Tipado TypeScript:

```ts
type FacialEnrollmentWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.facial_enrollment.v1',
	{
		stepId: string;
		success: boolean;
		authenticationId: string;
		context: 'AUTHENTICATION_ID';
		assetId: string;
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "3f2f8df8-424a-4489-95eb-105f7f3e5efb",
	"type": "com.idv_suite.api.workflows.facial_enrollment.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:03:11.000Z",
	"data": {
		"stepId": "9076af18-3de1-4f03-b0f9-ad7deef5d01d",
		"success": true,
		"authenticationId": "auth-123",
		"context": "AUTHENTICATION_ID",
		"assetId": "asset-selfie-id"
	},
	"signature": "firma-hmac-base64"
}
```

#### 10) Verificacion facial 1:1

* **Tipo**: `com.idv_suite.api.workflows.facial_verification.v1`
* **Cuando se envia**: al validar identidad contra un enrollment previo.
* **Para que sirve**: autenticacion o confirmacion de identidad.

Tipado TypeScript:

```ts
type FacialVerificationWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.facial_verification.v1',
	{
		stepId: string;
		success: boolean;
		authenticationId: string;
		authStatus: string;
		similarity: number;
		assets: string;
	}
>;
```

Notas:

* `authStatus` sigue abierto; el ejemplo conocido es `"Positive"`.
* `assets` en este evento se emite como `string` simple, no como `string[]`.

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "865fb4e2-65cb-48d9-96a8-157ec95ecef6",
	"type": "com.idv_suite.api.workflows.facial_verification.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:03:33.000Z",
	"data": {
		"stepId": "7242e76b-3271-4f1b-b702-27042512ec66",
		"success": true,
		"authenticationId": "auth-123",
		"authStatus": "Positive",
		"similarity": 0.95,
		"assets": "asset-selfie-id"
	},
	"signature": "firma-hmac-base64"
}
```

#### 11) Matching documental evaluado

* **Tipo**: `com.idv_suite.api.workflows.document_matching_evaluated.v1`
* **Cuando se envia**: al comparar dos fuentes documentales dentro del flujo.
* **Para que sirve**: medir consistencia entre documentos o entre capturas del mismo titular.

Tipado TypeScript:

```ts
type DocumentMatchingEvaluatedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.document_matching_evaluated.v1',
	{
		stepId: string;
		score: number;
		status: 'MATCHED' | 'NOT_MATCHED' | 'WEAK_MATCHED' | 'WEAK_NOT_MATCHED';
		fields: Record<
			string,
			{
				source?: string;
				target?: string;
				similarity: number;
				weight: number;
			}
		>;
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "8b3bd547-4fb7-47f0-962d-b7f7d77dbb7d",
	"type": "com.idv_suite.api.workflows.document_matching_evaluated.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:03:55.000Z",
	"data": {
		"stepId": "d537af32-42c1-44b4-aea5-2a8db31dff2f",
		"score": 0.94,
		"status": "MATCHED",
		"fields": {
			"documentNumber": {
				"source": "X1234567",
				"target": "X1234567",
				"similarity": 1,
				"weight": 1
			}
		}
	},
	"signature": "firma-hmac-base64"
}
```

#### 12) Operacion finalizada

* **Tipo**: `com.idv_suite.api.workflows.operation_finished.v1`
* **Cuando se envia**: al cierre de la operacion (exito, rechazo, expiracion o error).
* **Para que sirve**: definir decision final y cerrar proceso de negocio.

Tipado TypeScript:

```ts
type OperationFinishedWebhook = WebhookEnvelope<
	'com.idv_suite.api.workflows.operation_finished.v1',
	{
		status: 'SUCCEEDED' | 'DENIED' | 'ERROR' | 'CANCELLED' | 'BLACKLISTED' | 'EXPIRED';
		reason?: string;
		content: Partial<{
			'passive-liveness': Array<{
				id: string;
				data: {
					success: boolean;
					diagnostic: string;
					assets?: string[];
				};
			}>;
			'injection-attack': Array<{
				id: string;
				data: {
					success: boolean;
					score: number;
					probability: number;
					status: 'REAL' | 'SPOOF';
					assets?: string[];
				};
			}>;
			'face-matching': Array<{
				id: string;
				data: {
					success: boolean;
					authStatus: string;
					similarity: number;
					assets?: string[];
				};
			}>;
			'id-extracted': Array<{
				id: string;
				data: {
					success: boolean;
					data: Record<string, unknown>;
					summary: Record<string, unknown>;
					assets?: string[];
				};
			}>;
			'facial-enrollment': Array<{
				id: string;
				data: {
					success: boolean;
					authenticationId: string;
					context: 'AUTHENTICATION_ID';
					assetId: string;
				};
			}>;
			'facial-verification': Array<{
				id: string;
				data: {
					success: boolean;
					authenticationId: string;
					authStatus: string;
					similarity: number;
					context: 'AUTHENTICATION_ID';
					assets: string;
				};
			}>;
			'document-matching': Array<{
				id: string;
				data: {
					success: boolean;
					score: number;
					status: 'MATCHED' | 'NOT_MATCHED' | 'WEAK_MATCHED' | 'WEAK_NOT_MATCHED';
					fields: Record<string, { source?: string; target?: string; similarity: number; weight: number }>;
				};
			}>;
			'anti-fraud-check': Array<{
				id: string;
				data: {
					success: boolean;
					type: string;
					status: string;
					reason?: string;
					assetId?: string;
					assetHash?: string;
					documentNumber?: string;
					documentType?: string;
					documentCountry?: string;
					deviceId?: string;
					assets?: string[];
				};
			}>;
			'civil-validation': Array<{
				id: string;
				data: {
					serviceResultCode?: number;
					serviceTime?: string;
					serviceResultLog?: string;
					serviceTransactionId?: string;
					serviceFacialAuthenticationResult?: number;
					serviceFacialSimilarityResult?: number;
					civilServiceCountry?: string;
					civilServiceNumber?: string;
					civilServiceData?: Record<string, unknown>;
				};
			}>;
			'document-validation': Array<{
				id: string;
				data: {
					id: string;
					attemptId?: string;
					acceptanceTime?: string;
					decisionTime?: string;
					code?: number;
					vendorData?: string;
					endUserId?: string;
					status?: string;
					reason?: string;
					reasonCode?: number;
					riskScore: number;
					person?: Partial<{
						gender: string;
						idNumber: string;
						lastName: string;
						firstName: string;
						citizenship: string;
						dateOfBirth: string;
						nationality: string;
						yearOfBirth: string;
						placeOfBirth: string;
						pepSanctionMatch: boolean | string;
					}>;
					document?: Partial<{
						type: string;
						state: string;
						number: string;
						country: string;
						validFrom: string;
						validUntil: string;
					}>;
				};
			}>;
		}>;
	}
>;
```

Ejemplo:

```json
{
	"specversion": "1.0",
	"id": "63f5f4f7-a4f7-43f7-aa27-f1f49f6fe131",
	"type": "com.idv_suite.api.workflows.operation_finished.v1",
	"source": "/operations/5f2d2e8d-0f0f-4cae-8fc1-55756b6d06f3",
	"time": "2026-03-06T12:04:10.000Z",
	"data": {
		"status": "DENIED",
		"reason": "FACE_MATCHING_FAILED",
		"content": {
			"face-matching": [
				{
					"id": "6fe21ca0-2914-49ac-b87c-d3c665304be4",
					"data": { "success": true, "authStatus": "Positive", "similarity": 0.93 }
				}
			]
		}
	},
	"signature": "firma-hmac-base64"
}
```
