Сервер корта и облако
Сервер корта стоит в клубе: принимает видео камер, ведёт счёт, режет хайлайты и считает статистику. В облако он отправляет только итоги — счёт, агрегаты, сжатую траекторию мяча, метаданные клипов и живой счёт. Сырые кадры и полная телеметрия мяча клуб не покидают.
Клубный токен
Все методы этого руководства вызываются с заголовком X-Club-Token: <клубный токен>. Токен один на клуб, его выдаёт AISOLUS при подключении кортов; в облаке хранится только хеш, поэтому потерянный токен не восстановить — только выпустить новый (прежний перестаёт работать). Неверный токен — 401 bad_club_token. Каждый метод видит только данные клуба своего токена: чужой матч, игра или бронь отвечают 404.
Связь с облаком сервер корта проверяет публичным GET https://адрес-вашего-сервера/api/v1/health.
Жизнь матча
- Перед игрой. Терминал корта узнаёт, чья сейчас бронь и кто в составе:
GET /v1/cv/court-bookings. Уровни игроков для подписи на экране —GET /v1/ingest/levels. Если идёт американо или турнир — игра или матч сетки на этом корте:GET /v1/ingest/quick-games,GET /v1/ingest/bracket. - Во время игры. Живой счёт для табло —
POST /v1/ingest/liveпри каждом изменении. Здоровье камер —POST /v1/cv/healthраз в 10 секунд; в ответ приходят отметки розыгрышей, сделанные игроками в приложении. Крупный план игрока можно отправить ему сразу —POST /v1/ingest/player-clip. - После игры. Сначала результат —
POST /v1/ingest/match-result; в ответеmatchId. Затем с этимmatchId: статистикаPOST /v1/ingest/match-stats, движениеPOST /v1/ingest/match-movement, розыгрышиPOST /v1/cv/rallyи итогPOST /v1/cv/match-summary, клипыPOST /v1/ingest/clips. Счёт американо и матча сетки —POST /v1/ingest/quick-games/{game_id}/scoreиPOST /v1/ingest/bracket/{match_id}/result.
Результат матча
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/match-result" \
-H "X-Club-Token: <клубный токен>" \
-H "Content-Type: application/json" \
-d '{
"court": "court4", "day": "2026-10-02", "time": "18:00",
"sets": "6:4,3:6,7:5",
"players": [
{"user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83", "team": 1},
{"phone": "+79000000001", "name": "Олег", "team": 1},
{"user_id": "a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15", "team": 2},
{"pass_code": "<код пропуска>", "team": 2}
],
"points_auto": 54, "points_manual": 3
}'
# → {"matchId": "8c0f4a52-…", "winnerTeam": 1, "ratingDeltas": {"3d6e9a10-…": 12, …},
# "bookingId": "b71c2e94-…", "voided": false}sets— очки «команда1:команда2» по сетам через запятую. Обе команды обязательны, игроков от 2 до 4.- Игрок задаётся одним из способов:
user_id(QR или привязка),phone(назван на терминале) илиpass_code(QR-пропуск, который корт не смог проверить сам). Незнакомый телефон заводит профиль: игрок увидит матч, когда войдёт в приложение с этим номером. Непроверенный пропуск, который не сошёлся, исключает игрока из матча, а матч — из рейтинга. - Равный счёт по сетам (не вели или не доиграли) — матч принимается, видео и статистика игрокам доступны, но в рейтинг он не идёт:
voided: true,ratingDeltasпустой. points_autoиpoints_manual— сколько очков засчитали камеры и сколько отметил человек. Если человек отметил больше, матч считается ручным и весит в рейтинге вдвое меньше.booking_id— бронь, из которой открыта сессия. Не указана — облако найдёт бронь по корту, дню и времени.
Результат матча не идемпотентен: каждый успешный вызов создаёт новый матч. Повторяйте его, только если ответа не было, и запоминайте matchId из первого успешного ответа.
Статистика, движение, розыгрыши
Эти методы ссылаются на match_id из ответа на результат и безопасны для повтора: повтор обновляет запись, а не создаёт вторую.
# статистика игроков
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/match-stats" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20", "avg_rally_shots": 6.8,
"players": [{"user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83", "max_ball_kmh": 112, "run_m": 2350,
"shots": [{"class": "смэш", "count": 7, "avgKmh": 104}]}]}'
# один розыгрыш (повтор того же rally_no перезаписывает его)
curl -X POST "https://адрес-вашего-сервера/api/v1/cv/rally" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20", "court": "court4", "rally_no": 1,
"t_start": 1759418000.0, "t_end": 1759418012.4,
"path": [[312, 401], [355, 380]], "path_fw": 1024, "path_fh": 768,
"shots": [{"t": 1759418001.2, "v_kmh": 74.0, "x": 312, "y": 401}],
"max_kmh": 96.0, "avg_kmh": 58.2, "winner_team": "A"}'- Статистика (
POST /v1/ingest/match-stats): игрок должен быть в составе матча. Игрокам с ненулевой скоростью мяча уходит уведомление о готовом разборе. - Движение (
POST /v1/ingest/match-movement): пробег, темп, пиковая скорость, рывки, доли зон, покрытие и сетка присутствия. Пустое поле не затирает известное. Игрок, узнанный по телефону уже после счёта, добавляется в состав без изменения рейтинга. - Розыгрыш (
POST /v1/cv/rally): траектория прорежена заранее (до 400 точек) и задана в пикселях кадраpath_fw × path_fh;winner_team—AилиB(принимаются и1,2). - Итог (
POST /v1/cv/match-summary): тепловая карта[x, y, n]и маршруты мяча[x1, y1, x2, y2, n]в пикселях кадра.
Клипы и загрузка файла
Клип отправляется в два сообщения с одним clip_id: сразу после матча — метаданные (видео ещё едет), затем, когда файл оказался в хранилище, — с object_key. По clip_id второе сообщение обновляет клип; пустые поля не затирают уже известные.
# 1. метаданные
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/clips" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"clips": [{"clip_id": "rally-012-cam1", "kind": "highlight", "title": "Смэш в сетку",
"duration_s": 12.5, "group": "rally-012", "camera": "Камера 1"}]}'
# 2. файл (если облако хранит видео у себя)
curl -X PUT "https://адрес-вашего-сервера/api/v1/ingest/clips/file?key=c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/rally-012-cam1.mp4" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: video/mp4" \
--data-binary @rally-012-cam1.mp4
# → {"ok": true, "key": "c1/8c0f…/rally-012-cam1.mp4", "bytes": 18734211}
# 3. ключ объекта — тем же clip_id
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/clips" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"clips": [{"clip_id": "rally-012-cam1",
"object_key": "c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/rally-012-cam1.mp4"}]}'- Моменты и ракурсы. Один момент, снятый несколькими камерами, — это главный клип (
kind: highlight) и ракурсы (kind: highlight_angle) с общимgroup;camera— подпись кнопки камеры в плеере. Вертикальная копия 9:16 для историй —kind: highlight_vertical. В карточке матча приложение показывает их одним моментом. - Файл.
PUT /v1/ingest/clips/fileработает, только когда облако хранит видео у себя; иначе ответ400 local_storage_disabled: видео хранится в объектном хранилище, и сервер корта выгружает его туда сам, а в облако отправляет толькоobject_key. Ключ обязан начинаться с id клуба токена (403 bad_key), размер ограничен (413 clip_too_large). - Ссылки игрокам облако выдаёт само: короткоживущие подписанные ссылки по
object_keyв карточке матча. - Хайлайт по ходу матча —
POST /v1/ingest/player-clip: файл уже в хранилище, игрок поuser_idили телефону получает уведомление со ссылкой на неделю. В матч этот момент попадает потом обычным порядком.
Трансляция и живой счёт
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/live" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"court": "court4", "score": "6:4 3:2 · 30-15", "teams": "Иван/Олег — Анна/Мария",
"set_no": 2, "on_air": true}'
# → {"ok": true, "ttl_s": 180}Счёт живёт 180 секунд: если корт замолчал, счёт сам пропадает с табло. on_air — идёт ли видеотрансляция корта. Живой счёт в очередь не ставится: устаревший счёт отправлять незачем. Как его читают сайты и табло — в руководстве «Трансляции и повторы».
Здоровье камер и отметки игроков
curl -X POST "https://адрес-вашего-сервера/api/v1/cv/health" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"court": "court4", "ts": 1759418400.0,
"cameras": [{"tag": "обзор-хайлайты", "online": true, "fps": 25.0, "last_frame_age_s": 0.1}],
"app": {"version": "1.2137", "uptime_s": 86400},
"host": {"disk_free_gb": 412.5, "disk_used_pct": 58.0, "gpu_temp_c": 61.0}}'
# → {"ok": true, "cameras": 1, "unknown_tags": [],
# "marks": [{"id": "0d4e8f21-…", "ts": 1759418400.5, "source": "app"}]}- Хранится одна последняя запись на корт. Прочитать её можно
GET /v1/cv/court/{court}/health— в ответе есть возраст замераageS. tag— роль камеры из согласованного списка (выдаётся при подключении). Незнакомый тег не отбрасывается, а возвращается вunknown_tags— проверьте конфигурацию камер.marks— отметки «в нарезку», которые игроки поставили в приложении (POST /v1/matches/mark) за последние 30 минут. Каждая выдаётся один раз — сохраните её у себя.
Брони корта
curl "https://адрес-вашего-сервера/api/v1/cv/court-bookings?court=court4&court=court5" \
-H "X-Club-Token: <клубный токен>"По каждому корту — идущая бронь (current) и ближайшие на три часа (upcoming), с составом: user_id, имя, короткое имя, кто бронировал. Телефонов в ответе нет. Имя корта сравнивается с кортами клуба в облаке; незнакомое возвращается с known: false, а не пропадает. Отменённые брони не возвращаются.
Американо и сетка турнира
Сервер корта сам узнаёт, какую игру американо (мексиканки) или какой матч сетки сейчас играют на корте, и присылает итог по автосчёту.
# американо: игра текущего раунда на корте ({"game": null} — нет)
curl "https://адрес-вашего-сервера/api/v1/ingest/quick-games?court=court4" -H "X-Club-Token: <клубный токен>"
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/quick-games/17/score" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"score1": 14, "score2": 10}'
# турнир: ближайший несыгранный матч сетки на корте ({"match": null} — нет)
curl "https://адрес-вашего-сервера/api/v1/ingest/bracket?court=court4" -H "X-Club-Token: <клубный токен>"
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/bracket/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/result" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"score": "6:4,3:6,7:5", "winner": 1}'
# повтор после обрыва связи → {"ok": true, "already": true}Счёт американо в уже завершённом американо — 409 finished. Очки игры — от 0 до 64.
Уровни и пропуск игрока
# уровни для терминала: до 16 id и до 16 телефонов через запятую
curl "https://адрес-вашего-сервера/api/v1/ingest/levels?ids=3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83&phones=%2B79000000001" \
-H "X-Club-Token: <клубный токен>"
# → {"levels": {"3d6e9a10-…": {"value": 3.25, "step": "Уверенный любитель", "games": 14},
# "+79000000001": {…}}}
# пропуск, который терминал не смог проверить сам
curl -X POST "https://адрес-вашего-сервера/api/v1/ingest/player-pass" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"code": "<код пропуска>"}'
# → {"userId": "3d6e9a10-…", "name": "Иван П."}Уровень отдаётся только игрокам с рейтингом. Пропуск: 404 pass_not_found — код не распознан, 410 pass_expired — срок истёк (QR в приложении обновляется сам).
Лицензия корта
Сервер корта получает подписанную лицензию на 30 дней, привязанную к отпечатку машины, и проверяет подпись вшитым открытым ключом.
curl -X POST "https://адрес-вашего-сервера/api/v1/license/court" \
-H "X-Club-Token: <клубный токен>" -H "Content-Type: application/json" \
-d '{"fingerprint": "<sha256 машины, 64 шестнадцатеричных символа>", "version": "1.2137"}'
# → {"alg": "Ed25519", "payload": "<base64>", "signature": "<base64>",
# "license": {"v": 2, "club_id": "c1", "expires_at": …, "modules": {…}, "pass_keys": {…}, …}}- Подписаны ровно байты JSON из
payload(base64) — проверяйте подпись по ним, а не по пересобранному JSON. - В лицензии — модули клуба по кортам и ключи дней для проверки QR-пропусков без интернета.
403 fingerprint_revoked— администратор клуба отозвал эту машину.503 license_key_not_configured— в облаке не настроен ключ подписи; это не отказ в лицензии.
Очередь и повторы
Интернет в клубе ненадёжен, поэтому клиент сервера корта AISOLUS устроен так: запрос, который не удалось отправить, кладётся в очередь на диске и доотправляется позже по порядку. Пока не пришёл matchId, всё, что на него ссылается, ждёт в очереди. Живой счёт и здоровье в очередь не попадают — они устаревают сразу.
| Метод | Повтор безопасен? | Почему |
|---|---|---|
POST /v1/ingest/match-result | Нет | Каждый вызов создаёт матч |
POST /v1/ingest/match-stats | Да | Значения перезаписываются |
POST /v1/ingest/match-movement | Да | Обновление по игроку |
POST /v1/ingest/clips | Да, при заданном clip_id | Обновление по clip_id; без него каждый раз новый клип |
POST /v1/cv/rally | Да | Перезапись по rally_no |
POST /v1/cv/match-summary | Да | Один итог на матч |
POST /v1/ingest/bracket/{match_id}/result | Да | Повтор отвечает already: true |
POST /v1/ingest/quick-games/{game_id}/score | Да | Перезапись счёта с откатом прежних очков |
POST /v1/ingest/player-clip | Повтор шлёт уведомление ещё раз | — |