Skip to content
Integration Guide

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

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

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

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

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.
  1. Read the raw request body exactly as received. Do not parse or re-serialize it before verification.
  2. Read X-Timestamp and reject requests outside the accepted time window, such as older than 5 minutes.
  3. Build the signed value as X-Timestamp + "." + raw request body.
  4. Compute HMAC_SHA256(signedValue, webhookSecret) and encode the result as lowercase hexadecimal.
  5. Compare the computed value with X-Signature-V2 using a timing-safe comparison.
  6. Use event_id for idempotency so retries do not process the same event twice.
  7. Return a 2xx response only after the webhook is accepted; otherwise return a non-2xx response.

Retry Policy

Othento sends webhooks asynchronously. If the receiver is temporarily unavailable, delivery is retried automatically before the event is marked as failed.

Success

When the receiver returns a successful HTTP response, the webhook is marked as delivered.

Retry

If the receiver times out, cannot be reached, or returns an error response, Othento retries delivery after a short delay.

Failure

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

  1. Configure the application webhook URL, for example with webhook.site.
  2. Enable application webhooks.
  3. Enable step webhooks when testing step.completed and step.failed.
  4. Trigger a manual or automatic verification flow.
  5. Confirm the receiver received a POST request with the expected webhook_type.
  6. Return a 2xx response after the signature and event payload are accepted.