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:
| Поле | Описание |
|---|---|
id | UUID запроса. При повторной попытке генерируется заново. |
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, если данных нет.