Спецификация формата исходящих HTTP-запросов от Sports Platform к вашему приёмнику и инструкция по проверке подписи.
При наступлении подписанного события (публикация новости, удаление или смена видимости прогноза и т. д.) платформа отправляет POST-запрос с JSON-телом на URL вашего приёмника.
POST| Заголовок | Значение | Назначение |
|---|---|---|
Content-Type | application/json | Тип тела |
User-Agent | SportsContentPlatform/1.0 | Идентификация отправителя |
X-Webhook-Event | news.published и т. д. | Тип события (см. раздел 4) |
X-Webhook-Signature | HMAC-SHA256(body, secret) в hex | Подпись для верификации источника. Приходит, только если в админке задан секрет вебхука. |
X-Webhook-Signature просто не приходит — приёмник не сможет верифицировать источник. В таком случае попросите администратора задать секрет.
{
"event": "news.published",
"timestamp": "2026-04-29T14:33:00.123456+00:00",
"data": { ... }
}
event — тип события (совпадает с X-Webhook-Event).timestamp — момент отправки в ISO-8601 (UTC).data — данные события, структура зависит от event.| event | Когда отправляется |
|---|---|
news.published | Новость опубликована (вышла из модерации в публичную ленту) |
news.rejected | Новость отклонена модератором |
prediction.created | Создан экспертный прогноз |
prediction.deleted | AI-прогноз эксперта удалён — нужно удалить и на стороне сайта |
prediction.visibility_changed | У AI-прогноза эксперта изменилась публичная видимость (появился/скрылся на сайте) |
prediction.result_calculated | Рассчитан результат прогноза — выигран или проигран (поле result). Обновите статус прогноза на сайте |
math_prediction.generated | Расчётный матпрогноз (Poisson/Elo). Сейчас наружу приостановлено. |
match.updated | Обновлены данные матча |
test | Тестовое событие, отправляется по нажатию «Тест» в админке |
Подписка на события задаётся в карточке вебхука. Можно подписаться на * — тогда придут все события.
prediction.deleted и
prediction.visibility_changed относятся к AI-прогнозу эксперта
(тот, что отдаётся в /api/v1/predictions). Событие
math_prediction.generated — это отдельный расчётный матпрогноз
(xG/Poisson/Elo); его отправка наружу сейчас приостановлена.
Ниже — структура поля data для каждого события.
Конверт (event, timestamp) — как в разделе 3.
prediction.createdПоявился новый AI-прогноз (в т.ч. при ночной генерации). Прилетает
push-ом сразу при создании. Полные данные (ставки/обоснование) можно подтянуть из
/api/v1/predictions/feed или /by-match/{match_external_id}.
{
"event": "prediction.created",
"timestamp": "2026-06-01T03:00:00.000000+00:00",
"data": {
"id": 23571,
"match_external_id": "16231216",
"expert_id": 21,
"ui_status": "ready_to_publish",
"status": "ready",
"created_at": "2026-06-01T03:00:00.000000+00:00",
"updated_at": "2026-06-01T03:00:00.000000+00:00"
}
}
prediction.deletedПрогноз удалён у нас — удалите его и на сайте. Сопоставление — по
match_external_id (id матча в api-sport) + expert_id, либо по нашему id.
{
"event": "prediction.deleted",
"timestamp": "2026-05-31T08:00:00.000000+00:00",
"data": {
"id": 19090, // наш id прогноза (Prediction.id)
"match_external_id": "2461900", // id матча в api-sport (может быть null)
"expert_id": 83,
"deleted_at": "2026-05-31T08:00:00.000000+00:00"
}
}
prediction.visibility_changedСменилась публичная видимость прогноза. Если
ui_status стал непубличным (draft, below_threshold,
not_evaluated) — скройте прогноз на сайте; если публичным
(ready_to_publish, published, won, lost,
verified) — покажите.
{
"event": "prediction.visibility_changed",
"timestamp": "2026-05-31T08:00:00.000000+00:00",
"data": {
"id": 19090,
"match_external_id": "2461900",
"expert_id": 83,
"ui_status": "below_threshold", // см. список публичных статусов выше
"status": "draft", // сырой статус в БД
"updated_at": "2026-05-31T08:00:00.000000+00:00"
}
}
prediction.result_calculatedРассчитан итог прогноза: выигран или
проиграл. Шлётся и при автоматической сверке (после завершения матча),
и при ручной правке статуса в админке. Обновите статус прогноза у себя по полю
result. По прогнозам без однозначного исхода (возврат / нет результата)
событие не отправляется.
{
"event": "prediction.result_calculated",
"timestamp": "2026-06-08T14:23:45.123456+00:00",
"data": {
"id": 23571,
"match_external_id": "16236853", // id матча в api-sport (может быть null)
"expert_id": 21,
"status": "won", // won | lost
"ui_status": "won",
"result": "won", // основной флаг результата: won | lost
"final_score": "2-1", // итоговый счёт матча
"created_at": "2026-05-27T00:43:11.694000+00:00",
"updated_at": "2026-06-08T14:23:45.000000+00:00"
}
}
id, result) — повторная доставка
одного и того же результата безопасна (обрабатывайте как no-op).
match_external_id может быть null, если у матча нет id в api-sport
(например, ручной матч). Тогда сопоставляйте по нашему id прогноза.
X-Webhook-Signature.HMAC-SHA256(тело_запроса_как_байты, секрет) в hex-формате.import hmac
import hashlib
from fastapi import FastAPI, Header, Request, HTTPException
WEBHOOK_SECRET = b"ваш-секрет-минимум-16-символов"
app = FastAPI()
@app.post("/webhook")
async def receive(request: Request, x_webhook_signature: str = Header(None)):
body = await request.body() # сырые байты!
expected = hmac.new(WEBHOOK_SECRET, body, hashlib.sha256).hexdigest()
if not x_webhook_signature or not hmac.compare_digest(expected, x_webhook_signature):
raise HTTPException(403, "Invalid signature")
payload = await request.json()
# обработка payload["event"], payload["data"] ...
return {"ok": True}
<?php
$secret = 'ваш-секрет-минимум-16-символов';
$body = file_get_contents('php://input'); // сырое тело
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $received)) {
http_response_code(403);
exit('Invalid signature');
}
$payload = json_decode($body, true);
// $payload['event'], $payload['data'] ...
echo json_encode(['ok' => true]);
const express = require('express');
const crypto = require('crypto');
const SECRET = 'ваш-секрет-минимум-16-символов';
const app = express();
// raw body нужен ДО json-парсера
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.headers['x-webhook-signature'] || '';
const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
// constant-time сравнение
const a = Buffer.from(received, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(403).send('Invalid signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
// payload.event, payload.data ...
res.json({ ok: true });
});
app.listen(3000);
Платформа делает до 3 попыток доставки при ошибке. Это значит, что один и тот же event может прийти повторно. Чтобы не обрабатывать дубль — храните на своей стороне ключ дедупликации (например, event + data.id или data.match_id + data.date) и игнорируйте повторы.
Любой ответ HTTP 2xx считается успешной доставкой. Возвращайте 200 OK сразу после быстрой валидации — тяжёлую обработку запускайте в фоне, иначе платформа решит, что вы упали по таймауту, и пришлёт повтор.
event=test с подписью.