Адрес и доступ
Базовый адрес: https://mafiaspace.ru
Ключ выдаёт команда MafiaSpace отдельно. Заголовок каждого запроса:
Authorization: Bearer <ключ>. Для тела — Content-Type: application/json.
Ключ храните только на сервере, без браузера, мобильного клиента и логов.
Есть два отдельных ключа: sandbox и production. Адрес API одинаковый;
среду определяет ключ. Тестовые игры не создают публичных игроков и не влияют
на настоящий рейтинг. Проверить клуб и среду: GET /v1/integrations/game-results/me.
Не отправляйте вымышленные примеры рабочим ключом.
Ключ закреплён за конкретным клубом: поле clubId в игре отсутствует, менять клуб через
запрос нельзя. me возвращает acceptGamesFrom — начало разрешённого периода
новых игр. Более старые игры отклоняются для защиты от повторной загрузки архива.
Быстрый старт
- Получите тестовый ключ, проверьте
me:environmentдолжен бытьsandbox. - Скачайте example-game.json и game.schema.json.
- Отправьте пример командой ниже. Первый ответ —
201, повтор —200. - Посмотрите тестовую таблицу через
GET /v1/integrations/game-results/standings. - Проверьте исправление и отмену. Затем используйте рабочий ключ для настоящих игр.
curl --fail-with-body --request PUT \
'https://mafiaspace.ru/v1/integrations/game-results/example-game-001' \
--header "Authorization: Bearer $MAFIASPACE_SANDBOX_TOKEN" \
--header 'Content-Type: application/json' \
--data-binary @example-game.json
Методы
| Метод | Путь | Результат |
|---|---|---|
| GET | /v1/integrations/game-results/me |
Клуб, среда, дата приёма, лимиты и ссылка на рейтинг |
| POST | /v1/integrations/game-results/validate |
Проверка состава, баллов и версии без сохранения игры |
| PUT | /v1/integrations/game-results/{externalGameId} |
Сохранение или полная замена результата |
| GET | /v1/integrations/game-results/{externalGameId} |
Текущая версия игры и статус |
| POST | /v1/integrations/game-results/{externalGameId}/void |
Отмена ошибочной игры |
| GET | /v1/integrations/game-results/standings |
Проверочная таблица только игр данного ключа/источника |
validate принимает { "externalGameId": "example-game-001", "game": <JSON игры> }.
Успех: { "valid": true, "effect": "write", "warnings": [], "game": {...}, "requestId": "..." }.
Для точного повтора effect равен repeat. Проверка не резервирует версию.
Обязательные данные игры
| Поле | Значение |
|---|---|
schemaVersion |
Число 1 |
revision |
Целое 1–2147483647. Первая версия 1, исправление — предыдущая + 1 |
playedAt |
Начало игры, RFC 3339 с часовым поясом: 2026-09-15T20:00:00+03:00 |
finishedAt |
Конец игры в том же формате; не раньше начала и не более 5 минут в будущем |
gameType |
Строка sport_mafia_10 |
winner |
civilians — красные, mafia — чёрные, draw — ничья |
players |
Ровно 10 игроков, уникальные места 1–10 и уникальные ID людей |
players[].externalPlayerId |
Постоянный ID игрока в системе клуба |
players[].nickname |
Ник на момент игры, 1–100 символов |
players[].seat |
Целое 1–10 |
players[].role |
civilian × 6, mafia × 2, don × 1, sheriff × 1 |
players[].isAlive |
true, если игрок остался в игре на момент окончания; иначе false |
players[].points.base |
Строка "1" победителю, "0" проигравшему; при ничьей всем "0" |
players[].points.bonus |
Сумма всех положительных допов, например "0.3"; без начислений "0" |
players[].points.penalty |
Величина всех вычитаемых штрафов, например "0.1"; без штрафов "0" |
Допустимы необязательные externalEventId (ID вечера) и eventTitle
(название, 1–200 символов). Они сохраняют контекст результата; календарная
запись не создаётся автоматически. Ночные действия, голосования и полный
протокол не требуются. Статистика таких действий из итогового результата не выводится.
ID игры в URL и ID игрока/вечера: 1–100 символов [A-Za-z0-9._-].
Для ID игры запрещены me, validate, standings. ID не меняется вместе с
ником, датой или названием вечера и не переиспользуется для другой игры/человека.
Если своих ID нет, создайте их один раз и сохраните в своей базе.
Баллы — строки с точкой, без знака минус, от 0 до 1000, максимум два
знака после точки. Все три компонента обязательны. null, неизвестный итог,
неполный состав, незнакомые поля и произвольный base отклоняются. При ничьей
все три компонента равны нулю — это правило действующего рейтинга MafiaSpace.
bonus должен уже включать все положительные начисления; не отправляйте туда
общий итог. Штраф не вычитайте из bonus: передавайте отдельно в penalty.
Как игра попадает в рейтинг
Сервер проверяет победителя и базовые баллы, затем считает:
- Баллы за игру = base + bonus − penalty.
- Рейтинг = сумма баллов × число побед ÷ число игр.
- Место в основной таблице присваивается после 10 игр; до этого игрок виден в квалификации. Победы определяются по роли и победившей команде.
Рабочий запрос создаёт обычную завершённую игру и проекцию статистики. В рейтинге
учитываются принятые по API игры вместе с остальными подходящими играми клуба.
Полный протокол при этом не выдумывается: источник помечен club_result_api.
Частные зачёты с отдельными регламентами и номинациями по ночным событиям не
назначаются автоматически; этот API пополняет текущий публичный рейтинг.
Одинаковый ник не объединяет людей. Постоянный externalPlayerId получает
постоянную карточку игрока MafiaSpace. Уже существующие игровые профили можно
связать через обычный механизм объединения профилей; аккаунт и права доступа
от отправки результата не появляются. Не создавайте новый ID при смене ника.
Ссылка на полный рейтинг конкретного клуба приходит в me и в ответе рабочего
сохранения как ratingUrl. Проверочная таблица standings относится только к вашему
источнику, поэтому может отличаться от полной клубной таблицы, где есть другие игры
и связанные профили.
Ответ на сохранение
Первая запись — 201. Исправление и точный повтор — 200. Пример ответа:
{
"externalGameId": "example-game-001",
"resultId": "<UUID приёма>",
"gameId": "<UUID игры, только для production>",
"revision": 1,
"status": "saved",
"environment": "production",
"statistics": "updated",
"visibility": "public",
"ratingUrl": "https://mafiaspace.ru/clubs/56a5df76-0f19-482b-98a7-563953b0b0e1/ratings",
"game": { "...": "полная нормализованная игра" },
"voidReason": null,
"requestId": "<ID запроса>"
}
В sandbox: gameId и ratingUrl равны null, statistics = sandbox_only,
visibility = private. Проверяйте environment в ответах.
updated подтверждает сохранение и обновление серверной проекции рейтинга в одной
транзакции. Сайт и приложение получают результат при следующем обновлении данных;
уже открытый или офлайн-экран сам по себе не подтверждает свежесть. Кэш рейтинга
на обслуживающем сервере сбрасывается после записи; соседний сервер может держать
данные до 45 секунд. Для разбора ошибки сохраните requestId. Годовой фильтр действующего рейтинга
использует UTC; передавайте реальное время с правильным смещением часового пояса.
Повтор, исправление, отмена
Сохраняйте исходящие запросы в очереди на своём сервере. При тайм-ауте, сетевой
ошибке или 5xx повторяйте тот же ID, revision и JSON: через 2, 5, 15, 60 секунд,
затем раз в 5 минут с небольшим случайным разбросом. Не повышайте версию ради повтора.
Удаляйте запрос из очереди после 200/201 с ожидаемой версией либо подтверждающего GET.
- Точная повторная отправка не меняет число игр или баллы. Порядок JSON-ключей
и мест в массиве, эквивалентные часовые пояса и запись
"0.3"/"0.30"нормализуются. - Для исправления получите GET, увеличьте
revisionна один и передайте полную исправленную игру через PUT. Старый вклад заменится новым, включая смену даты. - Одинаковая версия с разными данными и запоздавшая старая версия дают
409. - Отмена: POST
/v1/integrations/game-results/example-game-001/voidс{ "revision": 2, "reason": "Внесено по ошибке" }. Если игра уже имеет версию 2, отмена должна иметь версию 3.reason— 1–500 символов. - Отмена убирает игру из рейтинга и сохраняет историю. Точный повтор отмены —
200. GET возвращаетstatus: voided. Восстановление — через поддержку; новый ID для обхода отмены не создавайте. Игра из интеграции редактируется только этим API.
Лимиты: тело до 64 KiB, 60 запросов в минуту на источник, до 2 одновременно
на обслуживающем сервере. При 429 соблюдайте заголовок Retry-After.
Ошибки
Формат: {"error":{"code":"invalid_game","message":"invalid_game","fields":[]},"requestId":"..."}.
fields при ошибке поля содержит path и code. Ответ прокси при 5xx может быть не JSON.
| HTTP | Код | Действие |
|---|---|---|
| 400 | invalid_json |
Исправить JSON |
| 401 | invalid_token |
Проверить ключ; остановить очередь до исправления доступа |
| 403 | integration_disabled |
Доступ отключён, обратиться в MafiaSpace |
| 404 | result_not_found |
Проверить ID и среду |
| 405 | method_not_allowed |
Проверить HTTP-метод |
| 409 | revision_conflict, revision_gap, result_voided, historical_import_requires_mapping |
Прочитать GET и исправить причину; не создавать новый ID |
| 413 | payload_too_large |
Уменьшить тело |
| 415 | unsupported_media_type |
Передавать application/json |
| 422 | invalid_game, duplicate_player_identity |
Исправить поля, состав или баллы |
| 429 | rate_limited |
Повторить после Retry-After |
| 500/502/503/504 | internal_error / ответ прокси |
Повторить ту же версию |
Проверка перед запуском
В sandbox: проверка без записи → первая игра → повтор → исправление баллов →
смена ника с прежним ID → отмена. Количество игр в проверочной таблице:
0 → 1 → 1 → 1 → 1 → 0. Убедитесь, что очередь переживает перезапуск вашего сервера.
Перед первой рабочей отправкой проверьте me, дату приёма и отсутствие этих же
игр в уже загруженном архиве/других источниках. Один и тот же externalGameId
предотвращает повтор внутри источника; другой ID или другой источник — другая
запись, поэтому не отправляйте одну игру по двум каналам без сопоставления.
Ключи и поддержку предоставляет MafiaSpace. Передавать внутренний формат клубной системы не требуется: реализация ведётся по этой спецификации.