AI PadelAPI documentation Back to site

Court server and cloud

The court server sits in the club: it takes the camera video, keeps the score, cuts highlights and computes stats. It sends only results to the cloud — score, aggregates, a compressed ball trajectory, clip metadata and the live score. Raw frames and full ball telemetry never leave the club.

Club token

Every method in this guide is called with the X-Club-Token: <club token> header. There is one token per club, issued by AISOLUS when courts are connected; the cloud keeps only a hash, so a lost token cannot be recovered — only re-issued (the old one stops working). A wrong token — 401 bad_club_token. Each method sees only the data of the token's club: another club's match, game or booking returns 404.

The court server checks the cloud link with the public GET https://your-server/api/v1/health.

Match lifecycle

  1. Before the game. The court terminal learns whose booking it is and the line-up: GET /v1/cv/court-bookings. Player levels for the on-screen names — GET /v1/ingest/levels. For an americano or a tournament — the game or bracket match on this court: GET /v1/ingest/quick-games, GET /v1/ingest/bracket.
  2. During the game. Live score for scoreboards — POST /v1/ingest/live on every change. Camera health — POST /v1/cv/health every 10 seconds; the response carries rally marks made by players in the app. A player close-up can be sent to the player right away — POST /v1/ingest/player-clip.
  3. After the game. First the result — POST /v1/ingest/match-result, which returns matchId. Then, with that matchId: stats POST /v1/ingest/match-stats, movement POST /v1/ingest/match-movement, rallies POST /v1/cv/rally and the summary POST /v1/cv/match-summary, clips POST /v1/ingest/clips. Americano and bracket scores — POST /v1/ingest/quick-games/{game_id}/score and POST /v1/ingest/bracket/{match_id}/result.

Match result

curl -X POST "https://your-server/api/v1/ingest/match-result" \
  -H "X-Club-Token: <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": "Oleg", "team": 1},
    {"user_id": "a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15", "team": 2},
    {"pass_code": "<pass code>", "team": 2}
  ],
  "points_auto": 54, "points_manual": 3
}'
# → {"matchId": "8c0f4a52-…", "winnerTeam": 1, "ratingDeltas": {"3d6e9a10-…": 12, …},
#    "bookingId": "b71c2e94-…", "voided": false}

The match result is not idempotent: every successful call creates a new match. Retry only when there was no response, and keep the matchId of the first successful response.

Stats, movement, rallies

These methods refer to match_id from the result response and are safe to retry: a repeat updates the record rather than creating a second one.

# per-player stats
curl -X POST "https://your-server/api/v1/ingest/match-stats" \
  -H "X-Club-Token: <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": "smash", "count": 7, "avgKmh": 104}]}]}'

# one rally (a repeat of the same rally_no overwrites it)
curl -X POST "https://your-server/api/v1/cv/rally" \
  -H "X-Club-Token: <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"}'

Clips and file upload

A clip is sent in two messages with the same clip_id: right after the match — metadata (the video is still on its way), then, once the file is in storage — with object_key. The second message updates the clip by clip_id; empty fields do not erase known ones.

# 1. metadata
curl -X POST "https://your-server/api/v1/ingest/clips" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
       "clips": [{"clip_id": "rally-012-cam1", "kind": "highlight", "title": "Smash at the net",
                  "duration_s": 12.5, "group": "rally-012", "camera": "Camera 1"}]}'

# 2. the file (when the cloud stores video itself)
curl -X PUT "https://your-server/api/v1/ingest/clips/file?key=c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/rally-012-cam1.mp4" \
  -H "X-Club-Token: <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. the object key — with the same clip_id
curl -X POST "https://your-server/api/v1/ingest/clips" \
  -H "X-Club-Token: <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"}]}'

Live score

curl -X POST "https://your-server/api/v1/ingest/live" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"court": "court4", "score": "6:4 3:2 · 30-15", "teams": "Ivan/Oleg — Anna/Maria",
       "set_no": 2, "on_air": true}'
# → {"ok": true, "ttl_s": 180}

The score lives 180 seconds: if the court goes silent, it disappears from scoreboards by itself. on_air says whether the court video stream is on. The live score is never queued — a stale score is not worth sending. See Live scores and replays for reading it.

Camera health and player marks

curl -X POST "https://your-server/api/v1/cv/health" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"court": "court4", "ts": 1759418400.0,
       "cameras": [{"tag": "<camera role 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"}]}

Court bookings

curl "https://your-server/api/v1/cv/court-bookings?court=court4&court=court5" \
  -H "X-Club-Token: <club token>"

Per court — the current booking (current) and those within three hours (upcoming), with the line-up: user_id, name, short name, who booked. No phone numbers. The court name is matched against the club courts in the cloud; an unknown one comes back with known: false instead of disappearing. Cancelled bookings are not returned.

Americano and tournament bracket

# americano: current-round game on the court ({"game": null} — none)
curl "https://your-server/api/v1/ingest/quick-games?court=court4" -H "X-Club-Token: <club token>"
curl -X POST "https://your-server/api/v1/ingest/quick-games/17/score" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"score1": 14, "score2": 10}'

# tournament: next unplayed bracket match on the court ({"match": null} — none)
curl "https://your-server/api/v1/ingest/bracket?court=court4" -H "X-Club-Token: <club token>"
curl -X POST "https://your-server/api/v1/ingest/bracket/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/result" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"score": "6:4,3:6,7:5", "winner": 1}'
# repeat after a connection drop → {"ok": true, "already": true}

A score for an americano that is already finished — 409 finished. Game points are 0 to 64.

Levels and player pass

# levels for the terminal: up to 16 ids and up to 16 phones, comma-separated
curl "https://your-server/api/v1/ingest/levels?ids=3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83&phones=%2B79000000001" \
  -H "X-Club-Token: <club token>"
# → {"levels": {"3d6e9a10-…": {"value": 3.25, "step": "<step name>", "games": 14},
#               "+79000000001": {…}}}

# a pass the terminal could not verify itself
curl -X POST "https://your-server/api/v1/ingest/player-pass" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"code": "<pass code>"}'
# → {"userId": "3d6e9a10-…", "name": "Ivan P."}

Levels are returned only for rated players. Pass: 404 pass_not_found — code not recognized, 410 pass_expired — expired (the QR in the app refreshes itself).

Court license

curl -X POST "https://your-server/api/v1/license/court" \
  -H "X-Club-Token: <club token>" -H "Content-Type: application/json" \
  -d '{"fingerprint": "<machine sha256, 64 hex characters>", "version": "1.2137"}'
# → {"alg": "Ed25519", "payload": "<base64>", "signature": "<base64>",
#    "license": {"v": 2, "club_id": "c1", "expires_at": …, "modules": {…}, "pass_keys": {…}, …}}

Queue and retries

Club internet is unreliable, so the AISOLUS court server client works like this: a request that failed is put into an on-disk queue and resent later in order. Until matchId arrives, everything referring to it waits in the queue. Live score and health are never queued — they go stale immediately.

MethodSafe to retry?Why
POST /v1/ingest/match-resultNoEvery call creates a match
POST /v1/ingest/match-statsYesValues are overwritten
POST /v1/ingest/match-movementYesUpdate per player
POST /v1/ingest/clipsYes, with clip_id setUpdate by clip_id; without it a new clip each time
POST /v1/cv/rallyYesOverwrite by rally_no
POST /v1/cv/match-summaryYesOne summary per match
POST /v1/ingest/bracket/{match_id}/resultYesA repeat answers already: true
POST /v1/ingest/quick-games/{game_id}/scoreYesOverwrites the score, rolling back the previous points
POST /v1/ingest/player-clipA repeat sends the push again—