Court server: computer vision
Rallies with ball trajectory, match summary, camera health and court bookings. Writes use the club token; reads accept the club token or a player token.
Rally #
/v1/cv/rallyAccess: Club token Header X-Club-Token: <club token> — the club court server token.
A whole rally: timing, thinned ball trajectory in frame pixels, shots with speed, winning team. A repeat of the same rally_no overwrites the row — the court queue may resend after a connection drop.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
match_id | string (uuid) | yes | matchId |
court | string | no | Court name |
rally_no | integer | yes | Rally number — a repeat overwrites the row by it min 0 |
t_start | number | yes | Start, unix seconds |
t_end | number | yes | End, unix seconds |
duration_s | number | no | Duration, s; 0 — computed from t_start and t_enddefault 0.0 |
path | array [array [number]] | no | Thinned ball trajectory [x, y] in frame pixels, up to 400 points items up to 400 |
path_fw | integer | no | Trajectory frame width default 0 |
path_fh | integer | no | Trajectory frame height default 0 |
shots | array [object] | no | Shots, up to 500 items up to 500 |
shots[].t | number | yes | Shot time, unix seconds |
shots[].v_kmh | number | no | Speed, km/h default 0.0 |
shots[].x | integer | no | x in frame pixels default 0 |
shots[].y | integer | no | y in frame pixels default 0 |
max_kmh | number | no | Top speed, km/h default 0.0 |
avg_kmh | number | no | Average speed, km/h default 0.0 |
shots_count | integer | no | Shot count; 0 — length of shots default 0 |
winner_team | string, or null | no | Winning team: A or B (1 and 2 are accepted too) |
Example request
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,
"duration_s": 12.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,
"shots_count": 7,
"winner_team": "A"
}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 404 | match_not_found | Match not found, belongs to another club, or you are not a participant |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"ok": true, "rallyId": "1f7c2a90-3b4d-4e5f-8a6b-7c8d9e0f1a2b", "shots": 9}Match summary #
/v1/cv/match-summaryAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Match summary: rally count, speeds, heat map and ball routes. A repeat updates the summary, no second map is created. If there are rallies and shots, the cloud fills the match average shots per rally.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
match_id | string (uuid) | yes | matchId |
court | string | no | Court name |
rallies | integer | no | Rally count default 0 |
max_kmh | number | no | Top speed, km/h default 0.0 |
avg_kmh | number | no | Average speed, km/h default 0.0 |
total_shots | integer | no | Total shots default 0 |
heat | array [array [number]] | no | Heat map: [x, y, n] — cell in frame pixels and count, up to 8000 items up to 8000 |
trails | array [array [number]] | no | Ball routes: [x1, y1, x2, y2, n] — cell-to-cell move and count, up to 4000 items up to 4000 |
fw | integer | no | Frame width default 0 |
fh | integer | no | Frame height default 0 |
Example request
curl -X POST "https://your-server/api/v1/cv/match-summary" \
-H "X-Club-Token: <club token>" \
-H "Content-Type: application/json" \
-d '{
"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"court": "court4",
"rallies": 42,
"max_kmh": 118.0,
"avg_kmh": 61.5,
"total_shots": 310,
"heat": [[320, 240, 7]],
"trails": [[320, 240, 360, 260, 3]],
"fw": 1024,
"fh": 768
}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 404 | match_not_found | Match not found, belongs to another club, or you are not a participant |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"ok": true, "heat": 120, "trails": 64}Court health #
/v1/cv/healthAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Court health: cameras, software version, disk and GPU. The court server sends it every 10 seconds; only the latest state is kept. An unknown camera tag is not dropped but returned in unknown_tags. The response carries rally marks made by players in the app during the last 30 minutes (each is delivered once).
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
court | string | yes | Court name |
ts | number | no | Measurement time, unix seconds; 0 — cloud time default 0.0 |
cameras | array [object] | no | Cameras, up to 32 items up to 32 |
cameras[].tag | string | yes | Camera role — a tag from the agreed list (provided on connection) |
cameras[].model | string | no | Camera model |
cameras[].online | boolean | no | Camera online default false |
cameras[].fps | number | no | Frames per second default 0.0 |
cameras[].fails | integer | no | Read failures (cumulative) default 0 |
cameras[].reopens | integer | no | Reconnects (cumulative) default 0 |
cameras[].last_frame_age_s | number | no | Age of the last frame, s default 0.0 |
cameras[].bright | number | no | Frame brightness default 0.0 |
cameras[].temp_c | number, or null | no | Camera temperature, °C, if known |
cameras[].dropped | integer | no | Dropped frames (cumulative) default 0 |
cameras[].drop_ratio | number | no | Dropped frame ratio default 0.0 |
app | object | no | Court server software |
app.version | string | no | Software version |
app.uptime_s | integer | no | Uptime, s default 0 |
host | object | no | Court server hardware |
host.disk_free_gb | number, or null | no | Free disk, GB |
host.disk_used_pct | number, or null | no | Disk used, % |
host.gpu_temp_c | number, or null | no | GPU temperature, °C |
host.gpu_name | string | no | GPU |
Example request
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}],
"app": {"version": "1.2137", "uptime_s": 86400},
"host": {"disk_free_gb": 412.5, "gpu_temp_c": 61.0}
}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"ok": true,
"cameras": 4,
"unknown_tags": [],
"marks": [{"id": "0d4e8f21-6a3b-4c7d-9e1f-2a5b8c0d3e47", "ts": 1759418400.5, "source": "app"}]
}Match computer vision data #
/v1/cv/match/{match_id}Access: Club token or player token Either X-Club-Token: <club token> or Authorization: Bearer <player token>. If the club token is sent, it is the one checked.
Match rallies with trajectories and shots, plus the summary (heat map, routes) once the match is closed. With the club token only matches of that club are available.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
match_id | string (uuid) | yes | Match id |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | no | Court server club token |
Example request
curl -X GET "https://your-server/api/v1/cv/match/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20" \
-H "X-Club-Token: <club token>"Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | no_token | No token in the Authorization header |
| 401 | invalid_token | Token expired or invalid — refresh it via /v1/auth/refresh |
| 401 | bad_club_token | Invalid club token |
| 404 | match_not_found | Match not found, belongs to another club, or you are not a participant |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"matchId": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"clubId": "c1",
"court": "Court 4",
"summary": {
"rallies": 42,
"maxKmh": 118.0,
"avgKmh": 61.5,
"totalShots": 310,
"heat": [[320, 240, 7]],
"trails": [[320, 240, 360, 260, 3]],
"fw": 1024,
"fh": 768
},
"rallies": [
{
"no": 1,
"tStart": 1759418000.0,
"tEnd": 1759418012.4,
"durationS": 12.4,
"maxKmh": 96.0,
"avgKmh": 58.2,
"shotsCount": 7,
"winnerTeam": "A",
"path": [[312, 401], [355, 380]],
"pathFw": 1024,
"pathFh": 768,
"shots": [{"t": 1759418001.2, "vKmh": 74.0, "x": 312, "y": 401}]
}
]
}Latest court health #
/v1/cv/court/{court}/healthAccess: Club token or player token Either X-Club-Token: <club token> or Authorization: Bearer <player token>. If the club token is sent, it is the one checked.
The latest court state and its age in seconds (ageS). With the club token — a court of that club; with a player token pass club_id.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
court | string | yes | Court name as on the court server |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
club_id | string | no | Club id |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | no | Court server club token |
Example request
curl -X GET "https://your-server/api/v1/cv/court/court4/health?club_id=c1" \
-H "X-Club-Token: <club token>"Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | no_token | No token in the Authorization header |
| 401 | invalid_token | Token expired or invalid — refresh it via /v1/auth/refresh |
| 401 | bad_club_token | Invalid club token |
| 400 | club_id_required | club_id is required with a player token |
| 404 | court_health_not_found | No health data from this court yet |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"clubId": "c1",
"court": "Court 4",
"ts": 1759418400.0,
"ageS": 4.2,
"cameras": [
{
"tag": "<camera role tag>",
"model": "",
"online": true,
"fps": 25.0,
"fails": 0,
"reopens": 0,
"last_frame_age_s": 0.1,
"bright": 0.0,
"temp_c": null,
"dropped": 0,
"drop_ratio": 0.0
}
],
"app": {"version": "1.2137", "uptime_s": 86400}
}Court bookings now #
/v1/cv/court-bookingsAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Bookings on the club courts: the current one and those within the next three hours, with the line-up (no phone numbers). Courts are passed as a repeated court parameter (up to 16) with names as on the court server; without it — all club courts. A court unknown to the cloud comes back with known=false. Cancelled bookings are not returned.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
court | array [string] | no | Court name as on the court server items up to 16 |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Example request
curl -X GET "https://your-server/api/v1/cv/court-bookings?court=court4" \
-H "X-Club-Token: <club token>"Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"clubId": "c1",
"now": "2026-10-02 18:05",
"courts": {
"court4": {
"known": true,
"current": {
"id": "b71c2e94-0a3f-4d58-8e26-9f1d3c5a7b40",
"court": "Court 4",
"day": "2026-10-02",
"start": "18:00",
"end": "19:30",
"durationMin": 90,
"current": true,
"players": [
{
"user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83",
"name": "Ivan Petrov",
"short": "Ivan P.",
"booker": true
}
]
},
"upcoming": []
}
}
}