Webhooks

Получайте на свой сервер уведомления о риске посетителя, проверяйте подпись запроса и смотрите журнал доставок.

Webhook — это HTTP POST-запрос, который Fingprint отправляет на ваш сервер, когда по посетителю появляется оценка риска. Так сервер узнаёт о потенциально опасном посетителе без отдельного запроса к API.

Webhook привязан к одному API-ключу и получает события только по запросам с этим ключом. К одному ключу можно привязать несколько webhooks.

Что понадобится

  • API-ключ в выбранной группе и его приватный ключ. Если не сохранили приватный ключ, можете создать новую пару ключей.
  • Адрес на вашем сервере, который принимает POST-запросы. Адрес должен использовать HTTPS и разрешаться в публичный IP-адрес. Адреса в локальной или частной сети не подходят.

Подготовьте обработчик

Fingprint отправляет JSON в теле запроса и подпись в заголовке Authorization. Подпись — это HMAC-SHA256 от исходного тела запроса, где секрет — приватный ключ. Значение передаётся в шестнадцатеричном виде, без префикса.

Проверяйте подпись по исходным байтам тела, до разбора JSON: после повторной сериализации подпись может не совпасть. Пример на Node.js с Express:

import crypto from 'node:crypto'
import express from 'express'

const app = express()
const privateKey = process.env.FINGPRINT_PRIVATE_KEY!

app.post('/webhooks/fingprint', express.raw({ type: '*/*' }), (req, res) => {
  const expected = crypto.createHmac('sha256', privateKey).update(req.body).digest('hex')
  const received = req.get('Authorization') ?? ''

  const isValid =
    Buffer.byteLength(received) === Buffer.byteLength(expected) &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))

  if (!isValid) {
    return res.sendStatus(401)
  }

  const event = JSON.parse(req.body.toString('utf8'))

  if (event.event === 'webhook.verification') {
    return res.status(200).json({ challenge: event.data.challenge })
  }

  console.log(event.event, event.data.visitor_id)

  res.sendStatus(200)
})

app.listen(3000)

На событие webhook.verification ответьте кодом 2xx и JSON с полученным значением challenge. Так Fingprint проверяет, что обработчик готов принимать запросы. Для событий риска достаточно ответа 2xx после проверки подписи. Долгую обработку выполняйте после ответа: Fingprint ждёт ответ не больше 10 секунд.

Создайте webhook

Откройте Webhooks и нажмите Новый Webhook. Заполните поля:

  • URL — адрес обработчика, например https://example.com/webhooks/fingprint;
  • API-ключ — выберите ключ, по запросам которого нужно получать события;
  • Описание — необязательно. Опишите для чего предназначен конкретный Webhook.

Нажмите Создать Webhook. Fingprint отправит проверочный запрос webhook.verification. Пока идёт проверка, события риска не отправляются. После ответа с правильным challenge webhook станет активным.

Если в группе ещё нет ключей, в поле выбора ключа нажмите Создать API-ключ. После создания сохраните приватный ключ — он будет показан один раз. Затем вы вернётесь к форме webhook: введённые данные сохранятся, а новый ключ будет выбран.

Привязанный ключ у существующего webhook изменить нельзя. Чтобы получать события по другому ключу, создайте новый webhook.

Проверьте подтверждение адреса

В списке Webhooks проверьте, что созданный webhook стал активным. Если проверка не прошла, откройте Журнал и проверьте ответ обработчика на webhook.verification: в JSON ответа должно быть поле challenge с тем же значением, что в запросе. Значение действует 15 минут.

Проверочный запрос отправляется один раз, без автоматических повторов. Исправьте обработчик и запустите повторную проверку в кабинете.

При изменении URL webhook нужно подтвердить новый адрес. До подтверждения события риска не отправляются. Ранее подтверждённый webhook можно отключить и снова включить без новой проверки, если URL не изменился.

Проверьте доставку

Убедитесь, что webhook активен, и выполните запрос идентификации с привязанным ключом. Затем в таблице Webhooks нажмите Журнал в строке webhook.

Журнал доставки показывает каждую попытку отправки: время, событие, Visitor ID, код ответа вашего сервера и время ответа. Если код ответа пустой, сервер не ответил: например, соединение не установилось.

Над таблицей видны сводные показатели за последние 24 часа: Доставлено за 24 ч, Сбоев за 24 ч и Ср. время ответа. Сбоем считается ответ с ошибкой или отсутствие ответа.

Не каждый запрос идентификации приводит к webhook. Событие visitor.risk_snapshot_update отправляется, только когда меняется оценка риска посетителя. Если в журнале нет доставок, это не всегда означает ошибку.

События

Тип события указан в поле event.

СобытиеВ кабинетеКогда отправляется
visitor.risk_snapshot_updateОбновление снимка рискаТолько после запроса идентификации, при котором изменилась оценка риска посетителя. Для нового посетителя — только если обнаружен риск: уровень риска выше no, режим инкогнито, опасный браузер или VPN.
webhook.verificationПроверка адресаПри создании webhook, изменении URL или запуске повторной проверки. В ответ нужно вернуть полученный challenge.

Формат запроса

Тело запроса содержит общие поля и данные события в data:

ПолеОписание
idUUID запроса. При повторной попытке генерируется заново.
timestampВремя отправки в формате ISO 8601.
eventТип события.
dataДанные события. У события риска поле data.visitor_id совпадает со значением user из SDK. У проверочного события здесь только challenge.

webhook.verification

{
  "id": "0f8b6f2e-5d7a-4c1e-9b3a-2a6c1d4e8f90",
  "timestamp": "2026-10-07T10:15:30.000000Z",
  "event": "webhook.verification",
  "data": {
    "challenge": "c7a12e9f4b6038d25a91f0c6e3d847b2a56c09e18f3b7d4a6205c8e9f1a3b6d0"
  }
}

После проверки подписи верните HTTP 200 с JSON:

{
  "challenge": "c7a12e9f4b6038d25a91f0c6e3d847b2a56c09e18f3b7d4a6205c8e9f1a3b6d0"
}

visitor.risk_snapshot_update

{
  "id": "0f8b6f2e-5d7a-4c1e-9b3a-2a6c1d4e8f90",
  "timestamp": "2026-09-29T10:15:30.000000Z",
  "event": "visitor.risk_snapshot_update",
  "data": {
    "visitor_id": "a1b2c3d4e5f6",
    "risk": {
      "level": "medium",
      "has_incognito": false,
      "has_telegram_browser": false,
      "has_vpn": true,
      "has_danger_browser": false
    },
    "statistics": {
      "visits_count": 12,
      "incognito_visits_count": 0,
      "vpn_visits_count": 3,
      "unique_ip_count": 4,
      "unique_location_count": 2,
      "unique_device_count": 1
    },
    "latest_visit": {
      "at": "2026-09-29T10:15:28.000000Z",
      "ip_address": "203.0.113.10",
      "browser": "Chrome",
      "os": "Windows",
      "location": { "country_code": "RU", "city": "Москва" }
    },
    "latest_vpn": {
      "ip_address": "203.0.113.10",
      "detected_at": "2026-09-29T10:15:28.000000Z"
    },
    "updated_at": "2026-09-29T10:15:29.000000Z"
  }
}

risk.level принимает значения no, low, medium или high. Поля latest_visit, latest_visit.location и latest_vpn могут быть null, если данных нет.

На этой странице