Othento Webhooks
Receive real-time verification updates from the Othento platform. Webhooks are sent as HTTP POST requests, signed with HMAC-SHA256, and delivered through the webhook outbox worker.
Overview
Webhooks notify your backend when verification sessions or verification steps reach a final result. They are designed for asynchronous processing: receive the request, verify the signature, store the event, and return a 2xx response quickly.
This guide is for Othento webhook consumers. It follows the same integration ideas as modern webhook systems, including signed payloads, timestamp validation, idempotent event handling, and retries.
Configuration
Webhook behavior depends on the application webhook settings and the features included in the application's plan.
| Configuration | Behavior |
|---|---|
| Webhook delivery is disabled. | No webhook is sent. |
| Webhook delivery is enabled, but step-by-step delivery is not enabled for the plan. | Only final session webhooks are sent: session.completed or session.failed. |
| Webhook delivery is enabled, and the application plan allows step-by-step delivery. | Step webhooks are sent for each verification step, and final session webhooks are sent when the full session finishes. |
Event Types
Othento sends only four webhook event types.
| Webhook Type | When It Fires |
|---|---|
session.completed |
A verification session reaches final status Approved. |
session.failed |
A verification session reaches a final non-approved result such as Declined or In Review, or fails after processing. |
step.completed |
A verification step completes with result Approved. |
step.failed |
A verification step completes with a non-approved result or fails. |
Payload Envelope
Every webhook payload uses a shared envelope. Session events include features and steps. Step events include step.
| Field | Type | Description |
|---|---|---|
event_id |
string | Unique event identifier. Use it for idempotency. |
version |
string | Payload version. Current version is 1.0. |
webhook_type |
string | One of session.completed, session.failed, step.completed, step.failed. |
environment |
string | sandbox or live. |
timestamp |
number | Unix seconds when the webhook was created for delivery. |
created_at |
number | Unix seconds for the source session or step completion time. |
application_id |
string | Application that owns the webhook destination. |
session_id |
string | External verification session ID. |
status |
string | Human-readable session status at the time of delivery. |
status_code |
string | Stable machine-readable status code such as APPROVED or DECLINED. |
decision |
string | null | Present on session events. Final session decision. |
reason |
string | null | Present on session events. Human-readable completion or failure reason. |
workflow_id |
string | Workflow external ID when available. |
workflow_version |
number | Workflow version number. |
vendor_data |
string | null | Client data supplied when the session was created. |
metadata |
object | null | Optional session metadata. |
attempt_count |
number | Present on session events. Session attempt count. |
max_attempts |
number | Present on session events. Maximum allowed session attempts. |
activated_at |
number | null | Present on session events. Unix seconds when the session was activated. |
completed_at |
number | null | Present on session events. Unix seconds when the session completed. |
expires_at |
number | Present on session events. Unix seconds when the session expires. |
features |
array | Present on session events. List of verification features included in the workflow. |
steps |
array | Present on session events. Contains one item per verification step. |
step |
object | Present on step events. Contains the completed or failed step result. |
Step Object
| Field | Description |
|---|---|
step_id |
Stable step identifier, such as step_face_match. |
step_code |
Stable step code, such as step_face_match. |
step_name |
Human-readable step name. |
status |
Step result label such as Approved, Declined, or In Review. |
status_code |
Stable machine-readable status code such as APPROVED or DECLINED. |
decision |
Step decision such as Approved, Declined, or In Review. |
status_reason |
Human-readable reason for the step result. |
attempt_number |
Step attempt number. |
completed_at |
ISO timestamp when the step completed. |
confidence_score |
Step confidence score when available. |
result |
Typed result object. Shape depends on the step type. |
evidence |
Evidence files for this step. URLs are presigned and expire after 20 minutes. |
Evidence Object
| Field | Description |
|---|---|
expires_at |
ISO timestamp when the presigned URLs expire. |
items |
Evidence files such as document_front, document_back, best_frame, cropped, enhanced, portrait, or selfie. |
items[].url |
Presigned URL. The URL is temporary and should be downloaded before expiry. |
items[].content_type |
Evidence content type, for example image/jpeg. |
Payload Examples
Session Completed
{
"version": "1.0",
"event_id": "9c0c8b8a-1111-4222-9333-444444444444",
"webhook_type": "session.completed",
"environment": "sandbox",
"timestamp": 1774970000,
"created_at": 1774969994,
"application_id": "11111111-2222-3333-4444-555555555555",
"session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"reason": null,
"workflow_id": "66666666-7777-8888-9999-000000000000",
"workflow_version": 4,
"vendor_data": "user_42",
"metadata": { "tier": "premium" },
"attempt_count": 1,
"max_attempts": 3,
"activated_at": 1774969900,
"completed_at": 1774969994,
"expires_at": 1775056394,
"features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "AML"],
"steps": [
{
"step_id": "step_id_verification",
"step_code": "step_id_verification",
"step_name": "ID Verification",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"status_reason": null,
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:25.443172Z",
"confidence_score": 98.7,
"result": {
"document_type": "Identity Card",
"document_number": "SAMPLE-DOC-12345",
"personal_number": "123456789",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"date_of_birth": "1990-01-01",
"date_of_expiry": "2030-01-01",
"date_of_issue": "2020-01-01",
"gender": "F",
"nationality": "ESP",
"issuing_country": "ESP",
"address": "Madrid, Spain",
"extra_fields": null,
"confidence_score": 98.7,
"warnings": []
},
"evidence": {
"expires_at": "2026-05-17T09:14:25Z",
"items": [
{
"type": "document_front",
"url": "https://example.com/presigned/document-front",
"content_type": "image/jpeg"
},
{
"type": "portrait",
"url": "https://example.com/presigned/portrait",
"content_type": "image/jpeg"
}
]
}
},
{
"step_id": "step_liveness_check",
"step_code": "step_liveness_check",
"step_name": "Liveness Check",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"status_reason": null,
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:28.443172Z",
"confidence_score": 95.4,
"result": {
"method": "ACTIVE_3D",
"score": 95.4,
"liveness_passed": true,
"face_detected": true,
"multiple_faces": false,
"warnings": []
},
"evidence": {
"expires_at": "2026-05-17T09:14:28Z",
"items": [
{
"type": "best_frame",
"url": "https://example.com/presigned/best-frame",
"content_type": "image/jpeg"
}
]
}
},
{
"step_id": "step_face_match",
"step_code": "step_face_match",
"step_name": "Face Match",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"status_reason": null,
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:30.443172Z",
"confidence_score": 96.1,
"result": {
"score": 96.1,
"verified": true,
"face_detected": true,
"multiple_faces": false,
"warnings": []
},
"evidence": {
"expires_at": "2026-05-17T09:14:30Z",
"items": []
}
}
]
}Session Failed
{
"version": "1.0",
"event_id": "9c0c8b8a-1111-4222-9333-555555555555",
"webhook_type": "session.failed",
"environment": "sandbox",
"timestamp": 1774970000,
"created_at": 1774969994,
"application_id": "11111111-2222-3333-4444-555555555555",
"session_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"status": "Declined",
"status_code": "DECLINED",
"decision": "Declined",
"reason": "Low face match similarity",
"workflow_id": "66666666-7777-8888-9999-000000000000",
"workflow_version": 4,
"vendor_data": "user_99",
"metadata": { "tier": "premium" },
"attempt_count": 1,
"max_attempts": 3,
"activated_at": 1774969900,
"completed_at": 1774969994,
"expires_at": 1775056394,
"features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"],
"steps": [
{
"step_id": "step_id_verification",
"step_code": "step_id_verification",
"step_name": "ID Verification",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"status_reason": null,
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:25.443172Z",
"confidence_score": 98.7,
"result": {
"document_type": "Passport",
"document_number": "SAMPLE-DOC-12345",
"date_of_expiry": "2024-01-01",
"warnings": []
},
"evidence": {
"expires_at": "2026-05-17T09:14:25Z",
"items": []
}
},
{
"step_id": "step_face_match",
"step_code": "step_face_match",
"step_name": "Face Match",
"status": "Declined",
"status_code": "DECLINED",
"decision": "Declined",
"status_reason": "Low face match similarity",
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:30.443172Z",
"confidence_score": 32.0,
"result": {
"score": 32.0,
"verified": false,
"face_detected": true,
"multiple_faces": false,
"warnings": [
{
"feature": "FACE_MATCH",
"risk": "LOW_FACE_MATCH_SIMILARITY",
"additional_data": null,
"log_type": "error",
"short_description": "LOW_FACE_MATCH_SIMILARITY",
"long_description": null,
"step_id": "step_face_match"
}
]
},
"evidence": {
"expires_at": "2026-05-17T09:14:30Z",
"items": []
}
}
]
}Step Completed
{
"version": "1.0",
"event_id": "1a2b3c4d-1111-2222-3333-444444444444",
"webhook_type": "step.completed",
"environment": "sandbox",
"timestamp": 1774970100,
"created_at": 1774970094,
"application_id": "11111111-2222-3333-4444-555555555555",
"session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"status": "Approved",
"status_code": "APPROVED",
"workflow_id": "66666666-7777-8888-9999-000000000000",
"workflow_version": 4,
"vendor_data": "user_42",
"metadata": { "tier": "premium" },
"step": {
"step_id": "step_face_match",
"step_code": "step_face_match",
"step_name": "Face Match",
"status": "Approved",
"status_code": "APPROVED",
"decision": "Approved",
"status_reason": null,
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:30.443172Z",
"confidence_score": 96.1,
"result": {
"score": 96.1,
"verified": true,
"face_detected": true,
"multiple_faces": false,
"warnings": []
},
"evidence": {
"expires_at": "2026-05-17T09:14:30Z",
"items": []
}
}
}Step Failed
{
"version": "1.0",
"event_id": "1a2b3c4d-1111-2222-3333-555555555555",
"webhook_type": "step.failed",
"environment": "sandbox",
"timestamp": 1774970100,
"created_at": 1774970094,
"application_id": "11111111-2222-3333-4444-555555555555",
"session_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"status": "Declined",
"status_code": "DECLINED",
"workflow_id": "66666666-7777-8888-9999-000000000000",
"workflow_version": 4,
"vendor_data": "user_99",
"metadata": { "tier": "premium" },
"step": {
"step_id": "step_face_match",
"step_code": "step_face_match",
"step_name": "Face Match",
"status": "Declined",
"status_code": "DECLINED",
"decision": "Declined",
"status_reason": "Low face match similarity",
"attempt_number": 1,
"completed_at": "2026-05-17T08:54:30.443172Z",
"confidence_score": 32.0,
"result": {
"score": 32.0,
"verified": false,
"face_detected": true,
"multiple_faces": false,
"warnings": [
{
"feature": "FACE_MATCH",
"risk": "LOW_FACE_MATCH_SIMILARITY",
"additional_data": null,
"log_type": "error",
"short_description": "LOW_FACE_MATCH_SIMILARITY",
"long_description": null,
"step_id": "step_face_match"
}
]
},
"evidence": {
"expires_at": "2026-05-17T09:14:30Z",
"items": []
}
}
}Signature Verification
Each webhook is signed with HMAC-SHA256 using the active webhook secret configured for the application. The secret is never sent in the payload or headers.
| Header | Meaning |
|---|---|
Content-Type |
Always application/json. |
User-Agent |
ZkycWebhook/1.0. |
X-Webhook-Type |
Same value as webhook_type in the payload. |
X-Timestamp |
Unix seconds included in the main signature and used for replay protection. |
X-Signature |
Lowercase hexadecimal HMAC-SHA256 signature of X-Timestamp + "." + raw request body. |
X-Signature-V2 |
Same value as X-Signature. Prefer this header for verification. |
Recommended Verification
- Read the raw request body exactly as received. Do not parse or re-serialize it before verification.
- Read
X-Timestampand reject requests outside the accepted time window, such as older than 5 minutes. - Build the signed value as
X-Timestamp + "." + raw request body. - Compute
HMAC_SHA256(signedValue, webhookSecret)and encode the result as lowercase hexadecimal. - Compare the computed value with
X-Signature-V2using a timing-safe comparison. - Use
event_idfor idempotency so retries do not process the same event twice. - Return a
2xxresponse only after the webhook is accepted; otherwise return a non-2xxresponse.
Retry Policy
Othento sends webhooks asynchronously. If the receiver is temporarily unavailable, delivery is retried automatically before the event is marked as failed.
When the receiver returns a successful HTTP response, the webhook is marked as delivered.
If the receiver times out, cannot be reached, or returns an error response, Othento retries delivery after a short delay.
If all retry attempts fail, the webhook is kept with its final failure status so it can be reviewed.
To avoid duplicate processing, receivers should treat event_id as the unique event key. If the same event is received more than once, it should be handled only once.
Testing
- Configure the application webhook URL, for example with webhook.site.
- Enable application webhooks.
- Enable step webhooks when testing
step.completedandstep.failed. - Trigger a manual or automatic verification flow.
- Confirm the receiver received a
POSTrequest with the expectedwebhook_type. - Return a
2xxresponse after the signature and event payload are accepted.