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:
| Setting | What it does |
|---|---|
| Encrypt the POST request body with the secret key | The body arrives encrypted with the scenario secret key, see “Encryption” |
| Do not send personal data | Recognised and entered data are removed from the body, see “Mode without personal data” |
| Notify only if the session is finalized | SESSION_COMPLETE arrives once, when the session is finally complete |
| Send a test request | Sends a trial request to the given address |
Delivery
- Method
POST, headerContent-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_COMPLETEandSESSION_COMPLETEis 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.
| event | When it arrives | How to get the same through the API |
|---|---|---|
SESSION_COMPLETE | The KYC session result changed | kyc/session/status |
DATABASE_CHECKS_COMPLETE | All database checks of the session finished | kyc/session/databases |
QUESTIONNAIRE_COMPLETE | The user filled in the stage-1 questionnaire | kyc/questionnaire/sessions |
PD_ERASURE_COMPLETE | Erasure of the session personal data finished | kyc/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
faceReferencefield (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_COMPLETEcarries no recognised document data (ocrinresultsandhistory) and no questionnaire data: full name, date of birth, email, phone and field values (keyandtitleremain);QUESTIONNAIRE_COMPLETEcarries no entered values; thechecksresults 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:
- The key is the first 32 characters of the hexadecimal SHA-256 string of the scenario secret key.
- Decode
encryptedfrom base64. - The first 16 bytes are the initialisation vector (IV), the rest is the ciphertext.
- 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);
}