> 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/docs.facephi-en/products/idv-suite/flujos-and-integraciones/configuracion-tecnica-del-cliente/webhook.md).

# Webhook

## Webhook Documentation

### Objective

Webhooks allow your system to receive real-time notifications as a verification operation progresses:

* operation start,
* evidence captures (document, selfie, NFC),
* biometric evaluations,
* final result.

With this, you can update business states, trigger rules, and show traceability to the end user without continuously polling.

### How it is sent

1. You configure an HTTPS receiving URL in your integration.
2. The system sends events via `POST` with JSON payload.
3. Only the events you have subscribed to in your configuration are sent.
4. Each message includes a signature to validate authenticity.

Recommendation for your Endpoint:

* respond quickly with `2xx` when the message is accepted,
* process asynchronously when possible,
* handle idempotency using `id` of the event.

### Security and authenticity

Each webhook includes:

* header `Content-Type: application/json`,
* custom security headers (if you defined them),
* field `signature` within the payload.

The signature is calculated with HMAC SHA-256 over the event content and a shared integration key. You must validate this signature before considering the message trustworthy.

#### How it is calculated `signature`

1. The event is taken **without** the field `signature`.
2. It is serialized with [JSON Canonicalization Scheme (JCS, RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785).
3. HMAC-SHA256 is applied to the canonical JSON.
4. The digest is encoded in **base64**.

The payload is delivered already serialized in JCS (including `signature`), so that the body matches the signature contract.

#### How to verify it

Recommended steps (robust in any language):

1. Parse the received JSON.
2. Read and save `signature`.
3. Remove `signature` from the object.
4. Canonicalize the remaining object with JCS (RFC 8785).
5. Calculate `HMAC-SHA256` + base64 with the same shared integration key.
6. Compare in constant time with the received signature.

If you consume the raw body as-is, in runtimes that preserve key order when parsing/serializing, it is usually enough to remove `signature` before the HMAC. Even so, the documented formal verification is **JCS + HMAC-SHA256 + base64**.

### General webhook structure

All events follow the same base structure. The body is delivered in JCS order:

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

Field meanings:

* `id`: unique message identifier (use to avoid duplicates).
* `type`: event type (defines how to interpret `data`).
* `source`: originating operation of the event.
* `time`: UTC emission date/time.
* `data`: business content of the event.
* `signature`: message signature (HMAC-SHA256 in base64 over the canonical event without this field).

### TypeScript Base

All events share this base envelope:

```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;
};
```

Modeling notes:

* `specversion` today is always `"1.0"`.
* `id`, `time` and `signature` do not have a closed catalog.
* `source` always follows the pattern `/operations/{operationId}`.
* In each event, only the values that the current implementation explicitly sets are closed as an enum.
* When an external provider or an internal stage does not define a closed catalog, the field should be left as `string`, `Record<string, unknown>` or an equivalent open structure.

### Currently available events

#### 1) Operation started

* **Type**: `com.idv_suite.api.workflows.operation_started.v1`
* **When sent**: when creating/starting an operation.
* **What it's for**: open operation tracking and correlate with your systems.

TypeScript typing:

```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;
		};
	}
>;
```

Example:

```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) Terms consent

* **Type**: `com.idv_suite.api.workflows.terms_consent.v1`
* **When sent**: when accepting or rejecting terms.
* **What it's for**: legal traceability and Flow continuity rules.

TypeScript typing:

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

Example:

```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) Captured document (ID/OCR)

* **Type**: `com.idv_suite.api.workflows.id_captured.v1`
* **When sent**: when document capture and data extraction are completed.
* **What it's for**: populate document data and validate consistency.

TypeScript typing:

```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[];
	}
>;
```

Notes:

* `data` and `summary` do not have a closed schema in the current implementation.
* `assets` contains asset ids associated with the event.

Example:

```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 captured

* **Type**: `com.idv_suite.api.workflows.selfie_captured.v1`
* **When sent**: when Face Capture is completed.
* **What it's for**: mark biometric stage progress.

TypeScript typing:

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

Example:

```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 captured

* **Type**: `com.idv_suite.api.workflows.nfc_captured.v1`
* **When sent**: when there is NFC reading from the document.
* **What it's for**: enrich and cross-check document information.

TypeScript typing:

```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[];
	}
>;
```

Notes:

* Like in `id_captured`, `data` and `summary` they remain open.

Example:

```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) Passive Liveness evaluated

* **Type**: `com.idv_suite.api.workflows.passive_liveness_evaluated.v1`
* **When sent**: after evaluating the passive liveness check.
* **What it's for**: detect potential impersonation attempts.

TypeScript typing:

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

Notes:

* `diagnostic` does not have a closed enum in code; the known example is `"Live"`.

Example:

```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) Injection detection

* **Type**: `com.idv_suite.api.workflows.injection_attack_detected.v1`
* **When sent**: after evaluating injection/synthetic risk.
* **What it's for**: strengthen real-time anti-fraud controls.

TypeScript typing:

```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[];
	}
>;
```

Example:

```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) Facial Matching evaluated

* **Type**: `com.idv_suite.api.workflows.facial_authentication_evaluated.v1`
* **When sent**: when comparing the document face vs. the selfie.
* **What it's for**: validate biometric correspondence.

TypeScript typing:

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

Notes:

* `authStatus` does not have a closed enum in code. The known example is `"Positive"`.

Example:

```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) Facial enrollment

* **Type**: `com.idv_suite.api.workflows.facial_enrollment.v1`
* **When sent**: when registering a biometric identity.
* **What it's for**: enable future 1:1 authentications.

TypeScript typing:

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

Example:

```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) 1:1 facial verification

* **Type**: `com.idv_suite.api.workflows.facial_verification.v1`
* **When sent**: when validating identity against a previous enrollment.
* **What it's for**: authentication or identity confirmation.

TypeScript typing:

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

Notes:

* `authStatus` remains open; the known example is `"Positive"`.
* `assets` in this event it is emitted as `string` simple, not as `string[]`.

Example:

```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) Document Matching evaluated

* **Type**: `com.idv_suite.api.workflows.document_matching_evaluated.v1`
* **When sent**: when comparing two document sources within the Flow.
* **What it's for**: measure consistency between documents or between captures of the same holder.

TypeScript typing:

```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;
			}
		>;
	}
>;
```

Example:

```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) Operation finished

* **Type**: `com.idv_suite.api.workflows.operation_finished.v1`
* **When sent**: upon operation closure (success, rejection, expiration, or error).
* **What it's for**: define final decision and close business process.

TypeScript typing:

```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;
					}>;
				};
			}>;
		}>;
	}
>;
```

Example:

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