Webhooks

A webhook is a POST request the system sends to your server when something happens to a session or a questionnaire. It tells you the result without constant API polling.


Setup

The webhook address is set in the KYC scenario settings in the personal account, in the backend callback URL field. Each scenario has its own address. If the field is empty, no webhooks are sent for the scenario.

The same settings include:

SettingWhat it does
Encrypt the POST request body with the secret keyThe body arrives encrypted with the scenario secret key, see “Encryption”
Do not send personal dataRecognised and entered data are removed from the body, see “Mode without personal data”
Notify only if the session is finalizedSESSION_COMPLETE arrives once, when the session is finally complete
Send a test requestSends a trial request to the given address

Delivery

  • Method POST, header Content-Type: application/json; charset=utf-8.
  • Requests are not signed. To make sure a request came from the system, enable encryption: only the scenario secret key decrypts the body.
  • The response timeout is 30 seconds. The code and body of your response are ignored.
  • There are no retries. If a webhook did not arrive, get the current state with an API method (see the events table).
  • The order of DATABASE_CHECKS_COMPLETE and SESSION_COMPLETE is not guaranteed.
  • Handle events idempotently: the same event for the same session may arrive more than once.

Events

The event type is in the event field of the request body.

eventWhen it arrivesHow to get the same through the API
SESSION_COMPLETEThe KYC session result changedkyc/session/status
DATABASE_CHECKS_COMPLETEAll database checks of the session finishedkyc/session/databases
QUESTIONNAIRE_COMPLETEThe user filled in the stage-1 questionnairekyc/questionnaire/sessions
PD_ERASURE_COMPLETEErasure of the session personal data finishedkyc/session/erase

SESSION_COMPLETE

The body is the session data in the same form as the kyc/session/status response, plus the event field.

  • By default the event arrives on every change of the session result while the user is going through it. The user's repeated attempts produce new events.
  • With "Notify only if the session is finalized" the event arrives once: when the session lifetime is over (secondsToLive = 0) or the attempts are used up.
  • If the scenario has database checks, the final event arrives after they finish.
  • The faceReference field (comparison with the reference session) arrives in every mode.
{
  "event": "SESSION_COMPLETE",
  "sessionId": "00000000-0000-0000-0000-000000000000",
  "schemaId": "00000000-0000-0000-0000-000000000000",
  "clientKey": "client-0001",
  "status": "success",
  "secondsToLive": 0,
  "results": []
}

The full list of fields is on the Session status page. Status values are on the Session statuses page.

DATABASE_CHECKS_COMPLETE

Arrives when all database checks of the session have finished. Not sent for a session whose personal data were erased.

{
  "event": "DATABASE_CHECKS_COMPLETE",
  "sessionId": "00000000-0000-0000-0000-000000000000",
  "schemaId": "00000000-0000-0000-0000-000000000000",
  "clientId": "00000000-0000-0000-0000-000000000000",
  "results": [
    {
      "checker": "insolvencyStatus",
      "spent": 1230,
      "outcome": "performed",
      "notPerformedReason": null,
      "missingInputs": [],
      "passed": true
    }
  ],
  "responseTime": "2026-01-01T10:00:00.000Z"
}

The check result fields are described on the kyc/session/databases page. Read outcome first, then passed: a skipped check is not a clean one.

QUESTIONNAIRE_COMPLETE

Arrives when the user submitted the stage-1 questionnaire (see Two-stage verification: questionnaire → KYC). If the questionnaire scenario has database checks, the event arrives after they finish and carries the checks array.

{
  "event": "QUESTIONNAIRE_COMPLETE",
  "type": "questionnaire",
  "questionnaireSessionId": "00000000-0000-0000-0000-000000000000",
  "schemaId": "00000000-0000-0000-0000-000000000000",
  "clientKey": "client-0001",
  "clientUser": "user-0001",
  "status": "success",
  "createdAt": "2026-01-01T10:00:00.000Z",
  "fullName": "John Doe",
  "dob": "1990-01-01",
  "email": "user@example.com",
  "phone": "+70000000000",
  "fields": [
    { "key": "city", "title": "City", "value": "Moscow" }
  ],
  "checks": [
    {
      "checker": "insolvencyStatus",
      "status": "success",
      "outcome": "performed",
      "notPerformedReason": null,
      "missingInputs": [],
      "result": { "passed": true }
    }
  ]
}

status: "success" means the questionnaire answers were recorded. It is not a decision about the user.

PD_ERASURE_COMPLETE

Arrives once, when erasure of the session personal data has finished: on a kyc/session/erase request or when the session is deleted in the personal account.

{
  "event": "PD_ERASURE_COMPLETE",
  "sessionId": "00000000-0000-0000-0000-000000000000",
  "schemaId": "00000000-0000-0000-0000-000000000000",
  "clientKey": "client-0001",
  "pdErasedAt": "2026-03-02T10:15:30Z",
  "parts": {
    "text": { "state": "erased" },
    "media": { "state": "erased" },
    "person": { "state": "kept", "reason": "linked_session" },
    "questionnaire": { "state": "absent" }
  },
  "responseTime": "2026-03-02T10:15:30Z"
}

If the event was lost, call kyc/session/erase again: it answers erased with the same pdErasedAt.


Linked scenarios

If a session was created through linked scenarios, webhooks go to the address of the scenario the session started from and are encrypted with its secret key. The body has selectedSchemaId — the scenario selected by the document, or null.


Mode without personal data

With "Do not send personal data":

  • SESSION_COMPLETE carries no recognised document data (ocr in results and history) and no questionnaire data: full name, date of birth, email, phone and field values (key and title remain);
  • QUESTIONNAIRE_COMPLETE carries no entered values; the checks results arrive in full.

The full data are available through the API methods.


Encryption

Encryption works when "Encrypt the POST request body with the secret key" is enabled and the scenario has a secret key — the same one that encrypts clientKey.

The body of an encrypted request:

{
  "encrypted": "<base64>"
}

How to decrypt:

  1. The key is the first 32 characters of the hexadecimal SHA-256 string of the scenario secret key.
  2. Decode encrypted from base64.
  3. The first 16 bytes are the initialisation vector (IV), the rest is the ciphertext.
  4. Decrypt with AES-256-CBC and parse the result as JSON.
Node.js example
const crypto = require('crypto');

function decryptWebhook(body, secretKey) {
  const key = crypto.createHash('sha256').update(secretKey).digest('hex').substring(0, 32);
  const buf = Buffer.from(body.encrypted, 'base64');
  const decipher = crypto.createDecipheriv('aes-256-cbc', key, buf.subarray(0, 16));
  const text = Buffer.concat([decipher.update(buf.subarray(16)), decipher.final()]).toString('utf8');
  return JSON.parse(text);
}