События и вебхуки
Сеть сообщает о событиях POST-запросом на адрес вебхуков, который вы указали в кабинете. Адреса нет — забирайте события сами, см. ниже. Каждое событие — JSON такого вида:
{
"id": "evt_6f1c2b4e-8a3d-4c55-9b1e-2f0a7d9c1e44",
"type": "match.created",
"created_at": "2026-10-24T12:00:00Z",
"data": { "…": "…" }
}
Заголовки:
| Заголовок | Значение |
|---|---|
X-Network-Signature |
sha256=<hex> — HMAC-SHA256 сырого тела на секрете подписи вашего контура (webhook_secret) |
X-Network-Event |
Тип события, то же, что type |
Content-Type |
application/json |
Все события
| Тип | Когда | Что сделать |
|---|---|---|
like.received |
Человек из другого приложения лайкнул вашего пользователя (super: true — суперлайк, сообщение в from.super_like) |
Показать во «Вас лайкнули», суперлайк — первым и выделенным |
like.withdrawn |
Человек отменил лайк до мэтча или стал недоступен (reason: удаляет аккаунт, скрыт по жалобам, анкета отклонена проверкой, удалён). Вернулся — придёт like.received снова |
Убрать из «Вас лайкнули» |
match.created |
Взаимный лайк | Создать разговор, запомнить match_id |
message.created |
Новое сообщение вашему пользователю, уже с переводом | Показать, отправить пуш; при suspicious: true — с предупреждением |
message.read |
Собеседник прочитал сообщения вашего пользователя | Показать «прочитано» |
match.closed |
Собеседник закрыл мэтч, заблокировал или удалён из сети (peer_deleted: true) |
Закрыть разговор, история остаётся; при peer_deleted — вместо имени и фото «Аккаунт удалён» / «Больше не в сети» |
match.frozen |
собеседник удаляет аккаунт (reason: account_deleting, 30 дней может восстановить) или скрыт сетью по жалобам (reason: suspended) |
«Аккаунт удалён» или «Аккаунт недоступен», писать нельзя |
match.unfrozen |
собеседник восстановил аккаунт или его вернули после жалоб | Вернуть имя, фото и переписку |
match.erased |
собеседник удалил пару или заблокировал со стиранием | Удалить у себя переписку |
member.reported |
На вашего пользователя пожаловались | Проверить по своим правилам |
member.suspended |
Сеть скрыла вашего пользователя по жалобам до разбирательства, его мэтчи заморожены | Разобрать жалобы: вернуть (reinstate) или заблокировать у себя |
member.rejected |
Анкета не прошла проверку сети: reason, photo если дело в фото или обложке, video если дело в ролике |
Попросить пользователя поправить анкету и прислать её снова |
photo_check.updated |
новый итог проверки фото сетью — после попытки, спора или решения модератора | Показать человеку итог; прошла — прислать заявку verification с check_id |
ping |
Вы нажали «Отправить проверочное событие» в кабинете | Ответить 2xx — так проверяется адрес и подпись |
Поля каждого события — в справочнике.
Проверка подписи
Обязательна: без неё кто угодно сможет прислать вам «сообщение» от имени сети. Считайте HMAC от сырого тела запроса — до разбора JSON — и сравнивайте за постоянное время.
PHP
<?php
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('PEREVAL_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_NETWORK_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
Python (Flask)
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["PEREVAL_WEBHOOK_SECRET"].encode()
@app.post("/pereval/hook")
def hook():
body = request.get_data() # сырое тело
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Network-Signature", "")):
abort(401)
event = request.get_json()
handle(event)
return "", 204
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/pereval/hook", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", process.env.PEREVAL_WEBHOOK_SECRET).update(req.body).digest("hex");
const got = req.get("X-Network-Signature") ?? "";
if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
handle(event);
res.sendStatus(204);
});
Go
func perevalHook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("PEREVAL_WEBHOOK_SECRET")))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Network-Signature"))) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
var event struct {
ID string `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
_ = json.Unmarshal(body, &event)
handle(event)
w.WriteHeader(http.StatusNoContent)
}
Повторы и дубли
- Ответ 2xx — событие доставлено. Отвечайте быстро; долгую работу делайте в фоне.
- Любой другой ответ или нет ответа за 10 секунд — повтор через 10 с, 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, затем раз в сутки, всего до 3 суток.
- Одно и то же событие может прийти дважды. Храните обработанные
idи пропускайте повторы:
CREATE TABLE pereval_events (id text PRIMARY KEY, received_at timestamptz DEFAULT now());
-- в обработчике, в одной транзакции с изменениями:
INSERT INTO pereval_events (id) VALUES ($1) ON CONFLICT DO NOTHING; -- 0 строк → уже обработано
- В релизном контракте 1.0.0 ожидающие события одного получателя сохраняют причинный порядок
seq, включая повторы. Разные получатели обрабатываются независимо. Истёкшие (7 дней) и окончательно не доставленные события не переигрываются бесконечно: после простоя сверьте мэтчи и историю с актуальным состоянием сети. Приостановка площадки ставит доставку на паузу, но не удаляет outbox и не продлевает обычный срок хранения.
Забрать события самому
GET /v1/events?after=<seq>&limit=100
Отдаёт события вашего контура по порядку seq. Пригодится в двух случаях:
- у площадки нет адреса для вебхуков — бот, сайт на простом хостинге. Если адреса нет, сеть события не шлёт, только отдаёт по запросу;
- нужно восстановиться после сбоя.
{
"items": [
{ "seq": 41, "id": "evt_…", "type": "message.created", "created_at": "2026-10-24T12:00:00Z", "data": { "…": "…" } }
],
"next_after": 41
}
Первый запрос — с after=0, следующий — с after=next_after. Храните next_after у себя. data — те же, что в вебхуке этого типа. За раз — до 500 событий.
Забранное считается доставленным. События хранятся 7 дней. Для живой переписки удобно опрашивать раз в несколько секунд; общий лимит — 600 запросов в минуту на площадку.
Если ваш сервер недоступен
События копятся и доставляются после восстановления, в течение 3 суток. Хранятся они 7 дней: что не дошло вебхуком, заберите запросом GET /v1/events. Остальное восстановите запросами GET /v1/members/{id}/matches, GET /v1/members/{id}/likes и GET /v1/matches/{id}/messages.
События буста
boost.started, boost.extended, boost.paused, boost.resumed, boost.completed приходят через ту же подписанную очередь и дедуплицируются по id. Передайте их интеграции услуг; незнакомые события игнорируйте без ошибки. Текущее состояние сверяется через GET /v1/members/{external_id}/boost. События не являются подтверждениями просмотра человеком. Подробности.
Удаления, повторы и старый бэкап
Успешный HTTP-ответ не доказывает, что сеть уже записала ACK: при сбое событие может прийти снова. Применяйте изменение и сохраняйте обработанный id атомарно в своей БД до ответа 2xx. Дедупликация только в памяти процесса не подходит.
Для match.erased сохраняйте устойчивую отметку удаления по match_id вместе с удалением локального чата. Она переживает перезапуск и очистку обычного журнала обработанных событий. Запоздалые match.created, message.created и match.unfrozen не должны воссоздать стёртую переписку. Известное удаление — подтверждённый no-op с 2xx, а не бесконечные 503. Для неизвестного мэтча сначала нужна сверка: не восстанавливайте историю по одному запоздалому событию.
После восстановления старого бэкапа не открывайте переписку до сверки актуального состояния и применения более новых удалений. Outbox хранится только 7 дней и не является полным журналом удалений для 30-дневной копии. Сеть и Via используют отдельные эксплуатационные журналы; публичного универсального экспорта всех удалений для сторонних площадок этот API пока не предоставляет. Площадке нужен собственный план сохранения отметок и восстановления; при недостаточной полноте оставьте доступ закрытым и обратитесь в поддержку.