Техническая документация для клуба со своим сайтом
Автоматическая отправка игр на сайт клуба
Эта страница нужна клубу, у которого есть свой сайт и разработчик. Реализуйте один webhook endpoint, получите Bearer token после проверки, и MafiaSpace будет отправлять завершённые игры на ваш сайт.
Endpoint можно добавить позже, если разработчик ещё готовит интеграцию.
После одобрения токен передаётся разработчику клуба.
Завершённая игра уходит webhook-запросом на сайт клуба.
Заявка на webhook
Подключить свой сайт клуба
Заполните эту форму, если хотите получать завершённые игры из MafiaSpace на свой сайт. Если у клуба нет своего сайта, эта техническая страница вам не нужна.
- Публичная форма не создаёт token.
- Bearer token выдаёт MafiaSpace после проверки заявки.
- Webhook URL можно оставить пустым, если endpoint ещё не готов.
Схема обмена
Судья ведёт игру в приложении или переносит бумажный протокол.
Победитель, роли, фолы, лучший ход и очки фиксируются в протоколе.
MafiaSpace сохраняет завершённую игру как каноническую запись.
MafiaSpace отправляет JSON на сервер клуба.
Клуб создаёт или обновляет игру у себя и возвращает ссылку на внешнюю карточку.
Endpoint клуба
Клуб должен принять HTTPS-запрос от MafiaSpace:
POST /api/mafiaspace/games
Authorization: Bearer <token>
Content-Type: application/json
URL endpoint и Bearer token выдаются/согласуются отдельно для каждого клуба. Не принимайте запросы без токена и не размещайте токен в frontend-коде.
Endpoint должен ответить за 10 секунд. Успешным считается только HTTP-ответ 2xx с JSON-полем
ok: true; один лишь HTTP 200 без этого поля не подтверждает доставку.
Endpoint уже готов или нужна помощь с подключением? Оставьте заявку на этой странице.
Если у клуба нет своего сайта, эта документация не нужна: webhook endpoint реализуется именно на стороне сайта клуба.
Как клуб получает токен
Клуб передаёт MafiaSpace production URL своего endpoint. Оператор MafiaSpace создаёт интеграцию, получает одноразово показанный Bearer token и передаёт его разработчику клуба по защищённому каналу. После этого клуб сохраняет токен у себя как server-side secret.
При компрометации токен ротируется: MafiaSpace выпускает новый token, клуб заменяет его на сервере, старый token перестаёт приниматься. В логах и интерфейсах показывайте только короткий preview токена, а не полное значение.
Payload завершённой игры
Ниже — фактический контракт текущей отправки MafiaSpace. Все перечисленные верхнеуровневые ключи
присутствуют в JSON, но значение некоторых из них может быть null. Для приёма обязательно
обработать external_id; остальные данные сайт сохраняет настолько полно, насколько ему нужно.
external_idСтабильный id игры в MafiaSpace. Используйте его как ключ идемпотентности.
club_slugSlug клуба или площадки, например `example-club`.
played_atДата и время игры в ISO 8601.
tableНазвание/номер стола.
game_numberНомер игры внутри вечера, турнира или клуба.
winnercity, mafia или null, если победитель не определён. Значение draw сейчас не отправляется.
judge{ "name": "…" } или null. В текущей отправке нет judge.external_id и judge.nickname.
players[]Игроки по местам со ссылками на роли, очки, фолы и идентификаторы.
rolesКарта ролей по местам: ключ — строковое место, значение обычно don, mafia, sheriff или civilian.
pointsКарта числовых protocol points по players[].external_id; отдельные компоненты начисления не передаются.
foulsКарта количества обычных фолов по players[].external_id. Технические фолы и удаления отдельными полями сейчас не отправляются.
best_moveОбъект лучшего хода или null, когда автор не определён. В объекте — автор и от 0 до 3 названных мест в произнесённом порядке.
killed_firstСсылка на первого выбывшего с правом ЛХ или null. Обычно это автор best_move.
eventsМассив событий. Сейчас MafiaSpace отправляет пустой массив []; сайт не должен ожидать в нём ленту протокола.
В реальной отправке external_id — UUID игры в MafiaSpace, played_at — ISO-время
завершения (или начала/создания как fallback), game_number сейчас всегда null.
Игроки отсортированы по seat; external_id игрока — его исходный id, а если его нет — UUID места в MafiaSpace.
Лучший ход хранит фактически произнесённую последовательность: игрок может назвать от одного до трёх мест,
повторить одно место несколько раз или не оставить ЛХ. При пасе best_move.status равно
passed, при одном или двух номерах — partial. called_seats — источник
истины; called_players — удобная расшифровка и обработчик должен терпимо относиться к тому, что
она короче массива мест.
Завершённая игра и Test-send содержат полный стол из 10 игроков. Обработчик должен быть терпим
к null, пустым массивам и незнакомым строковым ролям; не отклоняйте payload только потому,
что необязательная часть протокола не заполнена.
Идемпотентность
`external_id` обязателен. Если игра с таким `external_id` уже существует на стороне клуба, обновите её. Если игры ещё нет, создайте новую. Это правило защищает от дублей при retry, повторной отправке после правки протокола или ручном повторе экспорта.
Ожидаемый ответ клуба
После успешного создания или обновления игры верните HTTP 200–299 и JSON с обязательным
ok: true:
{
"ok": true,
"external_id": "mafiaspace-game-2026-07-04-club-001",
"external_game_url": "https://example-club.example/games/mafiaspace-game-2026-07-04-club-001"
}
`external_game_url` нужен, чтобы MafiaSpace мог показать оператору или клубу ссылку на созданную запись. `external_id` рекомендуется вернуть тем же, что пришёл в запросе.
Расширенный пример payload
Кнопка Test-send отправляет технический payload на полный стол из 10 игроков, с
game_number: 1, judge.name и пустым events: []. Он нужен именно
для проверки endpoint, а не для создания реальной статистики.
{
"external_id": "mafiaspace-game-2026-07-04-club-001",
"club_slug": "example-club",
"played_at": "2026-07-04T19:30:00+03:00",
"table": "Main table",
"game_number": 1,
"winner": "city",
"judge": {
"external_id": "judge-001",
"nickname": "Judge Club"
},
"players": [
{
"seat": 1,
"external_id": "example-club-player-001",
"nickname": "Ночь",
"role": "civilian",
"points": 1.0,
"fouls": 0
},
{
"seat": 2,
"external_id": "example-club-player-002",
"nickname": "Луна",
"role": "mafia",
"points": 0.0,
"fouls": 2
},
{
"seat": 3,
"external_id": "example-club-player-003",
"nickname": "Комиссар",
"role": "sheriff",
"points": 1.2,
"fouls": 1
},
{
"seat": 4,
"external_id": "example-club-player-004",
"nickname": "Ветер",
"role": "civilian",
"points": 1.0,
"fouls": 0
},
{
"seat": 5,
"external_id": "example-club-player-005",
"nickname": "Рубин",
"role": "civilian",
"points": 1.0,
"fouls": 1
},
{
"seat": 6,
"external_id": "example-club-player-006",
"nickname": "Север",
"role": "don",
"points": 0.0,
"fouls": 0
},
{
"seat": 7,
"external_id": "example-club-player-007",
"nickname": "Искра",
"role": "civilian",
"points": 1.0,
"fouls": 0
},
{
"seat": 8,
"external_id": "example-club-player-008",
"nickname": "Карат",
"role": "mafia",
"points": 0.0,
"fouls": 1
},
{
"seat": 9,
"external_id": "example-club-player-009",
"nickname": "Маяк",
"role": "civilian",
"points": 1.0,
"fouls": 0
},
{
"seat": 10,
"external_id": "example-club-player-010",
"nickname": "Шторм",
"role": "civilian",
"points": 1.0,
"fouls": 0
}
],
"roles": {
"1": "civilian",
"2": "mafia",
"3": "sheriff",
"4": "civilian",
"5": "civilian",
"6": "don",
"7": "civilian",
"8": "mafia",
"9": "civilian",
"10": "civilian"
},
"points": {
"example-club-player-001": 1.0,
"example-club-player-002": 0.0,
"example-club-player-003": 1.2,
"example-club-player-004": 1.0,
"example-club-player-005": 1.0,
"example-club-player-006": 0.0,
"example-club-player-007": 1.0,
"example-club-player-008": 0.0,
"example-club-player-009": 1.0,
"example-club-player-010": 1.0
},
"fouls": {
"example-club-player-001": 0,
"example-club-player-002": 2,
"example-club-player-003": 1,
"example-club-player-004": 0,
"example-club-player-005": 1,
"example-club-player-006": 0,
"example-club-player-007": 0,
"example-club-player-008": 1,
"example-club-player-009": 0,
"example-club-player-010": 0
},
"best_move": {
"player_seat": 1,
"player_external_id": "example-club-player-001",
"called_seats": [2, 7, 9],
"called_players": [
{
"seat": 2,
"external_id": "example-club-player-002",
"nickname": "Луна"
},
{
"seat": 7,
"external_id": "example-club-player-007",
"nickname": "Искра"
},
{
"seat": 9,
"external_id": "example-club-player-009",
"nickname": "Маяк"
}
]
},
"killed_first": {
"seat": 1,
"external_id": "example-club-player-001",
"nickname": "Ночь"
},
"events": [
{
"type": "game_completed",
"at": "2026-07-04T21:05:00+03:00",
"text": "Игра завершена победой города"
}
]
}
Расшифровка JSON-полей
external_idУникальный id игры в MafiaSpace. По нему клуб создаёт или обновляет игру.
club_slugКороткое имя клуба/интеграции. Для тестового примера: `example-club`.
played_atКогда игра была сыграна. Формат ISO 8601 с часовым поясом.
tableНазвание или номер стола, если у клуба несколько столов.
game_numberНомер игры внутри вечера, турнира, сессии или клубного протокола.
winnercity для мирных, mafia для мафии или null. Ничьей отдельным значением сейчас нет.
judge.nameОтображаемое имя судьи. Поле judge может быть null.
players[].seatМесто игрока за столом от 1 до 10.
players[].external_idId игрока во внешней системе клуба. Если своего id нет, используйте стабильный id из MafiaSpace.
players[].nicknameНик игрока, который нужно показать на сайте клуба.
players[].roleОбычно civilian, sheriff, mafia или don. Не отклоняйте неизвестную строку.
players[].pointsЧисловые protocol points игрока. Это то же значение, что в карте points.
players[].foulsКоличество обычных фолов игрока.
rolesКарта ролей по местам. Ключ — номер места строкой, значение — роль.
pointsКарта очков по `external_id` игрока. Удобно для быстрого импорта без обхода `players[]`.
foulsКарта фолов по `external_id` игрока.
best_move.statuspassed, если игрок не оставил ЛХ; partial, если назвал один или два номера. Если поля нет, сохранены три вызова.
best_move.player_seatМесто игрока, который оставляет ЛХ. Обычно совпадает с `killed_first.seat`.
best_move.player_external_idId игрока, который оставляет ЛХ. Обычно совпадает с `killed_first.external_id`.
best_move.called_seatsОт нуля до трёх мест в произнесённом порядке. Повторы допустимы: ЛХ 444 передаётся как [4, 4, 4].
best_move.called_playersРасшифровка названных игроков в том же порядке, включая повторы. При пасе массив пустой; он может быть короче called_seats, поэтому ориентируйтесь на места.
killed_first.seatМесто первого выбывшего игрока с правом ЛХ.
killed_first.external_idId первого убитого игрока.
killed_first.nicknameНик первого убитого игрока.
eventsСейчас всегда пустой массив. Поля events[] зарезервированы для будущего расширения.
okВ ответе клуба: `true`, если игра успешно создана или обновлена.
external_game_urlВ ответе клуба: ссылка на страницу созданной/обновлённой игры на сайте клуба.
Если игрок не оставил лучший ход
Иногда первый убитый игрок может отказаться от ЛХ или не назвать три места. В этом случае игра остаётся валидной, а лучший ход считается несыгранным.
{
"killed_first": {
"seat": 1,
"external_id": "example-club-player-001",
"nickname": "Ночь"
},
"best_move": {
"status": "passed",
"player_seat": 1,
"player_external_id": "example-club-player-001",
"called_seats": [],
"called_players": []
}
}
Для совместимости с внешними системами это означает: первый убитый сохранён, а лучший ход пустой. Если позже протокол исправят и появятся три места, повторный webhook с тем же `external_id` должен обновить игру.
Безопасность
- Принимайте только HTTPS-запросы.
- Проверяйте `Authorization: Bearer <token>` на сервере.
- Отклоняйте запросы без токена или с неверным токеном.
- Логируйте входящие запросы: время, `external_id`, статус обработки и причину ошибки.
- Не создавайте дубли: используйте `external_id` как уникальный ключ.
Retry
Если сайт клуба недоступен, отвечает не `2xx + {"ok": true}` (включая `404`) или не отвечает вовремя, MafiaSpace повторит отправку позже. Поэтому endpoint должен быть идемпотентным: повторный payload с тем же `external_id` обновляет существующую игру, а не создаёт дубль.
Следующий шаг
Подключить свой сайт клуба
Отправьте заявку, даже если endpoint ещё не готов. Мы согласуем формат, выдадим Bearer token после одобрения и проверим тестовую отправку на сайт клуба.
Подключить свой сайт