События и вебхуки

Сеть сообщает о событиях 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 пока не предоставляет. Площадке нужен собственный план сохранения отметок и восстановления; при недостаточной полноте оставьте доступ закрытым и обратитесь в поддержку.

Непонятно? Напишите разработчику

Ответ и продолжение переписки — на сайте по секретной ссылке, которую вы получите после отправки. Входящая почта не принимается.