Webhook для сайта клуба Подключить свой сайт

Техническая документация для клуба со своим сайтом

Автоматическая отправка игр на сайт клуба

Эта страница нужна клубу, у которого есть свой сайт и разработчик. Реализуйте один webhook endpoint, получите Bearer token после проверки, и MafiaSpace будет отправлять завершённые игры на ваш сайт.

1 Клуб оставляет сайт и контакт

Endpoint можно добавить позже, если разработчик ещё готовит интеграцию.

2 MafiaSpace выдаёт token

После одобрения токен передаётся разработчику клуба.

3 Игры приходят автоматически

Завершённая игра уходит webhook-запросом на сайт клуба.

Заявка на webhook

Подключить свой сайт клуба

Заполните эту форму, если хотите получать завершённые игры из MafiaSpace на свой сайт. Если у клуба нет своего сайта, эта техническая страница вам не нужна.

  • Публичная форма не создаёт token.
  • Bearer token выдаёт MafiaSpace после проверки заявки.
  • Webhook URL можно оставить пустым, если endpoint ещё не готов.
Сайт клуба и контакт разработчика Заполните минимум название клуба и один способ связи: Telegram или email.

Укажите Telegram или email. Достаточно одного способа связи.

Для связи нужен Telegram или email. Достаточно одного поля.

Схема обмена

1MafiaSpace судейство

Судья ведёт игру в приложении или переносит бумажный протокол.

2Завершение игры

Победитель, роли, фолы, лучший ход и очки фиксируются в протоколе.

3Сохранение в MafiaSpace

MafiaSpace сохраняет завершённую игру как каноническую запись.

4Outbound webhook

MafiaSpace отправляет JSON на сервер клуба.

5Сайт клуба

Клуб создаёт или обновляет игру у себя и возвращает ссылку на внешнюю карточку.

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_slug

Slug клуба или площадки, например `example-club`.

played_at

Дата и время игры в ISO 8601.

table

Название/номер стола.

game_number

Номер игры внутри вечера, турнира или клуба.

winner

city, 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

Номер игры внутри вечера, турнира, сессии или клубного протокола.

winner

city для мирных, mafia для мафии или null. Ничьей отдельным значением сейчас нет.

judge.name

Отображаемое имя судьи. Поле judge может быть null.

players[].seat

Место игрока за столом от 1 до 10.

players[].external_id

Id игрока во внешней системе клуба. Если своего 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.status

passed, если игрок не оставил ЛХ; partial, если назвал один или два номера. Если поля нет, сохранены три вызова.

best_move.player_seat

Место игрока, который оставляет ЛХ. Обычно совпадает с `killed_first.seat`.

best_move.player_external_id

Id игрока, который оставляет ЛХ. Обычно совпадает с `killed_first.external_id`.

best_move.called_seats

От нуля до трёх мест в произнесённом порядке. Повторы допустимы: ЛХ 444 передаётся как [4, 4, 4].

best_move.called_players

Расшифровка названных игроков в том же порядке, включая повторы. При пасе массив пустой; он может быть короче called_seats, поэтому ориентируйтесь на места.

killed_first.seat

Место первого выбывшего игрока с правом ЛХ.

killed_first.external_id

Id первого убитого игрока.

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` должен обновить игру.

Безопасность

Retry

Если сайт клуба недоступен, отвечает не `2xx + {"ok": true}` (включая `404`) или не отвечает вовремя, MafiaSpace повторит отправку позже. Поэтому endpoint должен быть идемпотентным: повторный payload с тем же `external_id` обновляет существующую игру, а не создаёт дубль.

Следующий шаг

Подключить свой сайт клуба

Отправьте заявку, даже если endpoint ещё не готов. Мы согласуем формат, выдадим Bearer token после одобрения и проверим тестовую отправку на сайт клуба.

Подключить свой сайт