# Вебхуки KYB

Система вебхуков KYB обеспечивает доставку уведомлений о событиях верификации юридических лиц на ваш сервер в режиме реального времени. Механизм доставки аналогичен вебхукам KYC: криптографическая подпись HMAC-SHA256, экспоненциальное увеличение интервала между повторными попытками, поддержка идемпотентности.

**Основные характеристики:**

- 12 типов событий, охватывающих полный жизненный цикл KYB-верификации
- События `kyb.business.verified`, `kyb.business.rejected`, `kyb.business.review_required` содержат **полный набор данных компании** в поле `data` (аналогично ответу API `GET /v1/kyb/businesses/{id}`): организационно-правовая форма, руководители, бенефициарные владельцы с UBO-полями, KYB-анкета `profile` (`email`, `phone`, `trade_name`, `business_type` и связанные поля), причины отклонения, дата последней верификации и прочие поля
- Криптографическая подпись HMAC-SHA256 для обеспечения безопасности доставки (заголовок `X-Onekyc-Signature`)
- Автоматические повторные попытки с экспоненциальным увеличением интервала (до 5 попыток)
- Ключи идемпотентности на основе SHA-256 для предотвращения дублирования
- Журнал доставки в разделе **Вебхуки** панели администратора

Настройка endpoint осуществляется в разделе **Вебхуки** панели администратора (тот же раздел, что и для KYC). При создании или редактировании endpoint выберите категорию **KYB events** и отметьте необходимые типы событий (или «Все события» для получения, в том числе, событий KYB).

---

## Как интерпретировать решение [#как-интерпретировать-решение]

- Наличие данных компании, `company_status: active` и события `kyb.step.completed` не означают успешную KYB-верификацию.
- Для `kyb.business.*` используйте `data.verification_status`: `verified`, `rejected` или `review`. Состояние `review` требует решения проверяющего и не является одобрением.
- Для `kyb.session.completed` используйте `data.result`: `approved` означает одобрение; `rejected` или `declined` — отказ. Поле `status` описывает состояние процесса. В исторических событиях отказ может иметь `status: failed`; завершённый процесс может иметь `status: completed` при любом финальном решении.
- Поле `decision_source` отличает `automatic` от `manual_review`. Для ручного решения передаются `decision_id` и, при наличии, `decision_reason_codes`. Отсутствие этих полей в старом событии не доказывает автоматическое одобрение.
- Ручное одобрение не изменяет результаты отдельных проверок. Анализируйте `verifications`, вложенные KYB-доказательства, `beneficiary_checks` и `decision_reasons`, если интеграции важно знать об отсутствии реестрового подтверждения, проблемах подписи или других сигналах. `ai_confirmation` не заменяет итоговый статус.
- `business_is_current_session: false` означает историческую сессию: её одобрение не должно заменять текущее решение по компании. Отсутствие поля означает, что связь с текущей сессией не установлена. Для изменений текущего решения компании используйте события `kyb.business.*`.
- Вложенный `business.verification_status` в финальном событии относится к решению этой сессии; исходный результат поиска сохраняется в доказательствах. При отказе коды ручного решения передаются отдельно от технических результатов проверок.

Сохранение решения, KYB-проекции компании и обязательных событий выполняется в одной транзакции. Ошибка сохранения события возвращает ошибку операции; HTTP-доставка выполняется асинхронно через outbox. HTTP 2xx подтверждает приём запроса вашим сервером, но не проверяет бизнес-обработку на вашей стороне.

Доставка может повторяться и приходить в другом порядке из-за повторных попыток. Учитывайте ID события, ID сессии и время решения. Два подписанных endpoint получают отдельную доставку одного события; одинаковые данные не означают два решения. Новое завершение после переоткрытия сессии создаёт новое событие, даже если итоговый статус не изменился.

## Перечень событий [#перечень-событий]

| Событие | Описание |
|---------|----------|
| `kyb.session.created` | KYB-сессия создана (пользователь перешёл по ссылке верификации) |
| `kyb.session.started` | KYB-сессия начата (пользователь приступил к заполнению данных) |
| `kyb.session.completed` | Получено финальное решение KYB-сессии (одобрение или отказ) |
| `kyb.session.expired` | Срок действия KYB-сессии истёк |
| `kyb.session.cancelled` | KYB-сессия отменена |
| `kyb.business.verified` | Компания успешно верифицирована |
| `kyb.business.rejected` | Компания отклонена |
| `kyb.business.review_required` | Компания направлена на ручную проверку оператором |
| `kyb.step.started` | Шаг верификации запущен (например, отправка запроса на ИИ-анализ) |
| `kyb.step.completed` | Шаг верификации завершён (например, ИИ-анализ) |
| `kyb.step.failed` | Шаг верификации завершился с ошибкой |
| `kyb.step.skipped` | Шаг верификации пропущен, потому что вышестоящая зависимость не прошла |

---

## Формат payload [#формат-payload]

Все события KYB используют единый формат конверта, аналогичный KYC:

```json
{
  "id": "evt_uuid",
  "type": "kyb.business.verified",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:05:00Z",
  "tenant_id": "tenant_uuid",
  "data": { ... }
}
```

---

## Пример: kyb.business.verified [#пример-kybbusinessverified]

Поле `data` содержит **полный снимок данных компании** (идентичен набору полей в ответе API `GET /v1/kyb/businesses/{id}`), включая блок `profile` с полями, введёнными во время KYB-флоу:

```json
{
  "id": "evt_kyb_biz_001",
  "type": "kyb.business.verified",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:05:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-001",
    "session_id": "sess_uuid",
    "last_session_id": "sess_uuid",
    "company_name": "Acme Corp Ltd",
    "registration_number": "KZ123456789",
    "tax_number": "123456789012",
    "country": "KZ",
    "legal_form": "ТОО",
    "company_status": "active",
    "status_date": "2015-06-15",
    "registration_date": "2015-06-15",
    "legal_address": "г. Алматы, ул. Примерная 1",
    "actual_address": "г. Алматы, ул. Примерная 1",
    "main_url": "https://acmecorp.kz",
    "profile": {
      "email": "compliance@acmecorp.kz",
      "phone": "+77271234567",
      "trade_name": "Acme",
      "website": "https://acmecorp.kz",
      "business_type": "b2b",
      "business_activity": "Software development",
      "ownership_structure": "Private company"
    },
    "director_name": "Иванов Иван Иванович",
    "director_title": "Генеральный директор",
    "directors": [{ "name": "Иванов Иван Иванович", "title": "Генеральный директор" }],
    "beneficiaries": [{ "name": "Петров П.П.", "share": "60%", "type": "natural_person", "country": "KZ", "pep": false }],
    "ai_confirmation": true,
    "ai_summary": "Company has been operating since 2015, no adverse findings.",
    "ai_sources": ["gosreestr.kz", "kgd.gov.kz"],
    "status_evidence_sources": ["gosreestr.kz"],
    "risk_score": 8.5,
    "rejection_reasons": [],
    "last_verified_at": "2026-02-24T10:05:00Z",
    "verification_status": "verified",
    "business_document_key": "business-docs/tenant_uuid/biz_uuid/charter.pdf",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:05:00Z"
  }
}
```

**Описание полей `data`:**

| Поле | Тип | Описание |
|------|-----|----------|
| `business_id` | string | UUID компании в системе OneKYC |
| `external_id` | string | Внешний идентификатор компании в вашей системе |
| `session_id` | string | UUID текущей сессии верификации |
| `last_session_id` | string | UUID последней сессии (совпадает с `session_id` при первой верификации) |
| `company_name` | string | Наименование компании |
| `registration_number` | string | Регистрационный номер компании |
| `tax_number` | string | ИНН / БИН |
| `country` | string | ISO-код страны регистрации |
| `legal_form` | string | Организационно-правовая форма |
| `company_status` | string | Статус компании в государственном реестре (`active`, `liquidating`, `liquidated` и др.) |
| `status_date` | string | Дата присвоения текущего статуса |
| `status_info` | string | Дополнительные сведения о статусе |
| `liquidation_date` | string | Дата ликвидации (при наличии) |
| `legal_address` | string | Юридический адрес |
| `actual_address` | string | Фактический адрес |
| `main_url` | string | Адрес официального сайта |
| `profile` | object | Поля анкеты компании, введённые во время KYB-флоу (`email`, `phone`, `trade_name`, `website`, `business_type`, данные деятельности, структура владения, идентификаторы) |
| `registration_date` | string | Дата государственной регистрации |
| `director_name` | string | ФИО руководителя |
| `director_title` | string | Должность руководителя |
| `directors` | array | Перечень руководителей `[{name, title}]` |
| `beneficiaries` | array | Перечень бенефициарных владельцев `[{name, share, type, country, pep}]` |
| `activities` | object | Виды деятельности `{main, additional}` |
| `ai_confirmation` | boolean | Результат ИИ-подтверждения |
| `ai_summary` | string | Краткое резюме ИИ-анализа |
| `ai_notes` | string | Дополнительные заметки ИИ-анализа |
| `ai_sources` | array | Источники, использованные при ИИ-анализе |
| `director_unavailable_reason_info` | string | Основание недоступности данных о руководителе |
| `beneficiaries_unavailable_reason_info` | string | Основание недоступности данных о бенефициарных владельцах |
| `activities_unavailable_reason_info` | string | Основание недоступности данных о видах деятельности |
| `main_url_unavailable_reason_info` | string | Основание недоступности сведений о сайте |
| `status_evidence_sources` | array | Источники сведений о статусе компании |
| `director_evidence_sources` | array | Источники сведений о руководителе |
| `beneficiaries_evidence_sources` | array | Источники сведений о бенефициарных владельцах |
| `activities_evidence_sources` | array | Источники сведений о видах деятельности |
| `main_url_evidence_sources` | array | Источники сведений о сайте |
| `risk_score` | number | Оценка риска (0–100) |
| `rejection_reasons` | array | Причины отклонения (для статуса `rejected`) |
| `last_verified_at` | string | Дата и время последней верификации (RFC 3339) |
| `verification_status` | string | Статус верификации: `verified`, `rejected`, `review`, `pending` |
| `business_document_key` | string | Ключ документа компании в объектном хранилище |
| `metadata` | object | Дополнительные метаданные |
| `created_at` | string | Дата и время создания записи (RFC 3339) |
| `updated_at` | string | Дата и время последнего обновления записи (RFC 3339) |

> Поля с пустыми или нулевыми значениями могут отсутствовать в payload.

---

## Пример: kyb.business.rejected [#пример-kybbusinessrejected]

Поле `data` содержит полные данные компании, включая массив `rejection_reasons` с указанием причин отклонения:

```json
{
  "id": "evt_kyb_biz_002",
  "type": "kyb.business.rejected",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:08:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-002",
    "session_id": "sess_uuid_2",
    "company_name": "Shell Corp",
    "registration_number": "XX999999",
    "country": "XX",
    "legal_form": "LLC",
    "risk_score": 72.3,
    "ai_confirmation": false,
    "rejection_reasons": [
      "company_not_found_in_registry",
      "high_risk_jurisdiction"
    ],
    "verification_status": "rejected",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:08:00Z"
  }
}
```

---

## Пример: kyb.business.review_required [#пример-kybbusinessreview_required]

Поле `data` содержит полные данные компании (включая `directors`, `beneficiaries`, `legal_form` и прочие поля):

```json
{
  "id": "evt_kyb_biz_003",
  "type": "kyb.business.review_required",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:10:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-003",
    "session_id": "sess_uuid_3",
    "company_name": "Ambiguous LLC",
    "registration_number": "KZ111222333",
    "country": "KZ",
    "legal_form": "АО",
    "directors": [{ "name": "Сидоров С.С.", "title": "Директор" }],
    "beneficiaries": [{ "name": "ООО Холдинг", "share": "100%", "type": "legal_entity" }],
    "risk_score": 45.0,
    "ai_confirmation": false,
    "verification_status": "review",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:10:00Z"
  }
}
```

---

## Пример: kyb.session.completed [#пример-kybsessioncompleted]

```json
{
  "id": "evt_kyb_sess_001",
  "type": "kyb.session.completed",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:04:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "session_id": "sess_uuid",
    "flow_id": "flow_uuid",
    "external_id": "company-001",
    "status": "completed",
    "scope": "kyb",
    "result": "approved",
    "business_id": "biz_uuid",
    "completed_at": "2026-02-24T10:04:00Z"
  }
}
```

---

## Верификация подписи [#верификация-подписи]

Механизм идентичен вебхукам KYC. Каждый запрос содержит заголовок:

```
X-Onekyc-Signature: t=<unix_timestamp>,v1=<hex_подпись>
```

При ротации секретного ключа дополнительно добавляется `v2=<новая_подпись>`. Подробное описание приведено в разделе [Вебхуки KYC](/ru/api/webhooks#проверка-подписи).

Алгоритм проверки подписи:

```javascript
const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = signatureHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
  const v1Signature = parts.find(p => p.startsWith('v1=')).split('=')[1];

  // Проверка актуальности timestamp (не старше 5 минут)
  if (Math.floor(Date.now() / 1000) - parseInt(timestamp) > 300) {
    throw new Error('Webhook timestamp is too old');
  }

  // Формирование строки для подписи: timestamp.raw_body
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1Signature));
}
```

---

## Политика повторных попыток [#политика-повторных-попыток]

При неуспешной доставке (ответ с кодом, отличным от 2xx, или превышение времени ожидания 30 секунд) система OneKYC выполняет повторную отправку с экспоненциальным увеличением интервала. Базовая задержка — 5 секунд, удваивается на каждой попытке, максимальный интервал — 24 часа:

| Попытка | Ориентировочная задержка |
|---------|--------------------------|
| 1 | ~5 с |
| 2 | ~10 с |
| 3 | ~20 с |
| 4 | ~40 с |
| 5 | Финальная попытка, затем DLQ, если доставка всё ещё неуспешна |

Задержки варьируются в пределах +/-20 % (jitter). Всего система выполняет до 5 общих попыток доставки. После исчерпания всех попыток событие помещается в очередь недоставленных сообщений (Dead Letter Queue). Повторную отправку можно инициировать вручную из раздела **Вебхуки** панели администратора.

## Итоговые данные о компании [#итоговые-данные-о-компании]

`kyb.session.completed` включает доступные `business_id`, `external_id`, `company_name` верхнего уровня и профиль `business` как для автоматических решений, так и для решений ревьюера. Профиль содержит доступные данные о регистрации, налогах, юрисдикции, адресе, организационно-правовой форме, директорах/бенефициарах и результаты поиска. Используются данные, собранные в этой сессии, или связанная с ней сохранённая компания; профиль компании из другой сессии не подставляется.

Включаются доступные актуальные верификации, анализ документов, загруженные `media_files`, причины решения, пользовательские шаги, сводки по рискам и устройствам, а также KYC-проверки бенефициаров. Результаты поиска сохраняют исход, причину, охват, использованные источники и следующие действия. Необработанные данные провайдеров, внутренние ссылки и пути хранения из анализа документов исключены.

Для физических лиц компании поля `is_pep` и `is_sanctioned` необязательны. `false` означает зафиксированный отрицательный результат; отсутствующее поле означает неизвестно или не проверялось. Не приводите отсутствие поля к `false`.

## Вебхуки комментариев ревью [#вебхуки-комментариев-ревью]

Новый комментарий к задаче KYC создаёт событие `verification.comment.created`, к задаче KYB — `kyb.review.comment.created`. Одинаковый контракт действует для админки клиента, платформенной консоли ревью и клиентского API. Комментарий не меняет решение проверки и не завершает сессию.

Подпишитесь на нужный тип события. Существующие подписки `*` также получают новые события. Используются обычные подпись вебхука, фильтрация sandbox/production, повторные попытки и журнал доставок. Возможны повторные доставки: устраняйте дубли по `id` события или `idempotency_key`. Порядок получения не гарантирован.

Поле `data` содержит `scope`, `task_id`, `session_id`, `flow_id`, `external_id` и объект `comment` с полями `id`, `content`, `created_at`, `author_type`. Для KYC передаётся `applicant_id`, для KYB — `business_id`, если он задан. Типы авторов: `admin_user`, `superadmin_user`, `api_key`, `system`. Идентификаторы и имена сотрудников в вебхук не включаются.

Пример данных события для KYC:

```json
{
  "scope": "kyc",
  "task_id": "550e8400-e29b-41d4-a716-446655440010",
  "session_id": "550e8400-e29b-41d4-a716-446655440001",
  "flow_id": "550e8400-e29b-41d4-a716-446655440002",
  "external_id": "customer_123",
  "applicant_id": "550e8400-e29b-41d4-a716-446655440099",
  "comment": {
    "id": "550e8400-e29b-41d4-a716-446655440011",
    "content": "Please provide a clearer document photo.",
    "created_at": "2026-10-16T08:00:00.123456Z",
    "author_type": "admin_user"
  }
}
```

Все комментарии задачи доступны клиенту-интегратору. Внутренние заметки решения `internal_notes` и сообщение повторной загрузки `client_message` — отдельные поля; они не создают события комментариев. События создаются только для новых комментариев; исторические комментарии не рассылаются.

Получение комментариев: `GET /tenant/v1/kyc/review-tasks/{task_id}/comments` с правом `reviews:read` или `GET /tenant/v1/kyb/review-tasks/{task_id}/comments` с правом `kyb.reviews:read`. Создание: `POST` по тому же пути с соответствующим правом записи. Успешный ответ создания дополнен `comment_id` и `created_at`, совпадающими с сохранённым комментарием и снимком в вебхуке. Каждый успешный POST создаёт новый комментарий; дедупликация вебхука не делает повторные запросы создания идемпотентными. При неопределённом результате запроса сверяйтесь с API комментариев до повторной отправки.

Комментарий, событие и запись outbox сохраняются одной транзакцией. Ошибка сохранения события откатывает создание комментария. HTTP-доставка выполняется асинхронно после фиксации транзакции: успешный ответ создания не подтверждает доставку вебхука клиенту; проверяйте журнал доставок.
