AI ПаделДокументация API На сайт

Сервер корта и облако

Сервер корта стоит в клубе: принимает видео камер, ведёт счёт, режет хайлайты и считает статистику. В облако он отправляет только итоги — счёт, агрегаты, сжатую траекторию мяча, метаданные клипов и живой счёт. Сырые кадры и полная телеметрия мяча клуб не покидают.

Клубный токен

Все методы этого руководства вызываются с заголовком X-Club-Token: <клубный токен>. Токен один на клуб, его выдаёт AISOLUS при подключении кортов; в облаке хранится только хеш, поэтому потерянный токен не восстановить — только выпустить новый (прежний перестаёт работать). Неверный токен — 401 bad_club_token. Каждый метод видит только данные клуба своего токена: чужой матч, игра или бронь отвечают 404.

Связь с облаком сервер корта проверяет публичным GET https://адрес-вашего-сервера/api/v1/health.

Жизнь матча

  1. Перед игрой. Терминал корта узнаёт, чья сейчас бронь и кто в составе: GET /v1/cv/court-bookings. Уровни игроков для подписи на экране — GET /v1/ingest/levels. Если идёт американо или турнир — игра или матч сетки на этом корте: GET /v1/ingest/quick-games, GET /v1/ingest/bracket.
  2. Во время игры. Живой счёт для табло — POST /v1/ingest/live при каждом изменении. Здоровье камер — POST /v1/cv/health раз в 10 секунд; в ответ приходят отметки розыгрышей, сделанные игроками в приложении. Крупный план игрока можно отправить ему сразу — POST /v1/ingest/player-clip.
  3. После игры. Сначала результат — 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}

Результат матча не идемпотентен: каждый успешный вызов создаёт новый матч. Повторяйте его, только если ответа не было, и запоминайте 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"}'

Клипы и загрузка файла

Клип отправляется в два сообщения с одним 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"}]}'

Трансляция и живой счёт

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"}]}

Брони корта

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": {…}, …}}

Очередь и повторы

Интернет в клубе ненадёжен, поэтому клиент сервера корта 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Повтор шлёт уведомление ещё раз—