Court server: data ingest
Match results, stats, movement, clips, live score, americano, tournament bracket, levels and player passes. Access: the club token in X-Club-Token.
Match result from the court #
/v1/ingest/match-resultAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Creates a match from the court score and recalculates the participants' rating. The response holds matchId: stats, movement, clips and rallies refer to it. A player is given by user_id, phone or a pass code; an unknown phone creates a new profile, and the player sees the match after signing in with that number. If the sets are level (score not kept or not finished), the match is accepted but does not count for rating (voided=true). If more points were entered by a person (points_manual) than by cameras (points_auto), the match is treated as manual and weighs half in the rating. The booking can be given in booking_id, otherwise the cloud finds it by court, day and time. The method is not idempotent: every successful call creates a new match.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
court | string | no | Court name |
day | string | yes | Match date YYYY-MM-DD |
time | string | yes | Start time HH:MM |
sets | string | yes | Score by sets “team1:team2”, comma-separated pattern ^\d+:\d+(,\d+:\d+)*$ |
players | array [object] | yes | Line-up: 2–4 players, both teams items from 2; items up to 4 |
players[].user_id | string (uuid), or null | no | Player id (from QR or binding) |
players[].phone | string, or null | no | Player phone if the id is unknown |
players[].team | integer | yes | Team: 1 or 2 min 1; max 2 |
players[].name | string | no | Name from the terminal — for a new profile |
players[].pass_code | string, or null | no | QR pass code the court could not verify itself length up to 120 |
booking_id | string (uuid), or null | no | Booking the session was opened from; omitted — the cloud finds it by court and time |
points_auto | integer, or null | no | Points scored by cameras min 0 |
points_manual | integer, or null | no | Points entered by a person min 0 |
Example request
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", "team": 1, "name": "Oleg"},
{"user_id": "a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15", "team": 2},
{"phone": "+79000000002", "team": 2, "name": "Maria"}
],
"points_auto": 54,
"points_manual": 3
}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 400 | bad_day_or_time | Invalid date or time (YYYY-MM-DD and HH:MM) |
| 400 | both_teams_required | Players of both teams are required |
| 404 | player_not_found: … | Player not found (the id or phone follows in the text) |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"matchId": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"winnerTeam": 1,
"ratingDeltas": {
"3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83": 12,
"a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15": -12
},
"bookingId": "b71c2e94-0a3f-4d58-8e26-9f1d3c5a7b40",
"voided": false
}Match stats #
/v1/ingest/match-statsAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Per-player match stats: top ball speed, distance, shots by type. The player is given by user_id or phone and must be in the match line-up. Players with a non-zero ball speed get an “analysis ready” push. A repeat overwrites the values.
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 from the match-result response |
avg_rally_shots | number, or null | no | Average shots per rally |
players | array [object] | yes | Per-player stats |
players[].user_id | string (uuid), or null | no | Player |
players[].phone | string, or null | no | Phone if the id is unknown |
players[].max_ball_kmh | integer, or null | no | Top ball speed, km/h |
players[].run_m | integer, or null | no | Distance, m |
players[].shots | array [object] | no | Shots by type: objects with class, count, avgKmh |
Example request
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}]
}
]
}'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 |
| 404 | player_not_in_match: … | The player is not in the match |
| 404 | player_not_found: … | Player not found (the id or phone follows in the text) |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"ok": true}Player movement #
/v1/ingest/match-movementAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Movement of each player from 3D court tracking: distance, work rate, top speed, sprints, zone shares, coverage, presence grid. Resending updates the rows without duplicates; an empty field does not erase a known value. A player recognized by phone but missing from the line-up is added without a rating change (joined). A guest without a phone is stored by name.
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 |
source | string | no | Tracking source length up to 16; default "court3d" |
players | array [object] | yes | Players, 1–8 items from 1; items up to 8 |
players[].user_id | string (uuid), or null | no | Player |
players[].phone | string, or null | no | Phone if the id is unknown |
players[].name | string | no | Name (for a guest without a phone) |
players[].team | integer | yes | Team: 1 or 2 min 1; max 2 |
players[].distance_m | number, or null | no | Distance, m min 0 |
players[].work_rate_m_per_min | number, or null | no | Work rate, m/min min 0 |
players[].top_speed_ms | number, or null | no | Top speed, m/s min 0 |
players[].sprints | integer, or null | no | Sprint count min 0 |
players[].zones | object, or null | no | Time share by zone, %: net, mid, back |
players[].coverage_percent | number, or null | no | Court coverage, % (0–100) min 0; max 100 |
players[].grid | array [array [number]] | no | Presence grid: [x, y, n], up to 4000 cells items up to 4000 |
Example request
curl -X POST "https://your-server/api/v1/ingest/match-movement" \
-H "X-Club-Token: <club token>" \
-H "Content-Type: application/json" \
-d '{
"match_id": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"source": "court3d",
"players": [
{
"user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83",
"team": 1,
"distance_m": 2410.5,
"work_rate_m_per_min": 38.2,
"top_speed_ms": 5.9,
"sprints": 14,
"zones": {"net": 42.0, "mid": 31.0, "back": 27.0},
"coverage_percent": 63.0,
"grid": [[2.5, 4.0, 18]]
},
{"name": "Guest", "team": 2, "distance_m": 1980.0}
]
}'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 |
| 404 | player_not_found: … | Player not found (the id or phone follows in the text) |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"ok": true, "count": 4, "added": 4, "updated": 0, "joined": 0}Clip metadata #
/v1/ingest/clipsAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Match clip metadata. A clip is sent twice: right after the match — metadata, then, once the video reaches storage — with object_key. Rows are matched by clip_id, so a repeat updates the clip instead of creating a duplicate; empty fields do not erase known values. Angles of the same moment share one group.
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 |
clips | array [object] | yes | Clips |
clips[].clip_id | string | no | Clip id on the court server — a repeat updates the clip by it |
clips[].kind | string | no | Kind: highlight, highlight_angle (angle), highlight_vertical (9:16 copy); empty — keep |
clips[].title | string | no | Title |
clips[].url | string | no | Public link if any; empty — the video is still on its way |
clips[].object_key | string | no | Storage object key — comes in the second message |
clips[].preview_url | string | no | Preview link |
clips[].duration_s | number | no | Duration, s (fractions allowed) default 0 |
clips[].group | string | no | Shared moment key for angles |
clips[].camera | string | no | Camera label for the player switch |
Example request
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"
},
{
"clip_id": "rally-012-cam2",
"kind": "highlight_angle",
"duration_s": 12.5,
"group": "rally-012",
"camera": "Camera 2"
}
]
}'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, "count": 2, "added": 2, "updated": 0}Upload clip file #
/v1/ingest/clips/fileAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Uploads the clip file (mp4) to the cloud when the cloud stores video itself. The body is raw mp4, not JSON. The key looks like “<club id>/<match>/<name>.mp4”: the first segment must equal the token club. After the upload send POST /v1/ingest/clips with this object_key — the cloud will hand players a short-lived link.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
key | string | yes | Object key “<club id>/<match>/<name>.mp4” |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body
Request body — the whole mp4 file (binary, not JSON).
Example request
curl -X PUT "https://your-server/api/v1/ingest/clips/file?key=c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/clip-001.mp4" \
-H "X-Club-Token: <club token>" \
-H "Content-Type: video/mp4" \
--data-binary @clip-001.mp4Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 400 | local_storage_disabled | The cloud does not store video locally — file upload is disabled |
| 403 | bad_key | The object key does not start with the token club id or contains .. |
| 413 | clip_too_large | The file exceeds the size limit |
| 507 | disk_full | No disk space in the cloud |
| 507 | write_failed | The file was not written |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{
"ok": true,
"key": "c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/clip-001.mp4",
"bytes": 18734211
}Player highlight during the match #
/v1/ingest/player-clipAccess: Club token Header X-Club-Token: <club token> — the club court server token.
A close-up of a player that is already in storage is pushed to the player right away, without waiting for the end of the match. The player is given by user_id or phone (an unknown phone creates a profile). The link in the push is valid for one week.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string (uuid), or null | no | Player |
phone | string, or null | no | Phone if the id is unknown |
name | string | no | Name for a new profile |
object_key | string | yes | Object key, starts with “<club id>/” |
title | string | no | Title in the push |
court | string | no | Court — for the caption |
duration_s | number | no | Duration, s default 0 |
Example request
curl -X POST "https://your-server/api/v1/ingest/player-clip" \
-H "X-Club-Token: <club token>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83",
"name": "",
"object_key": "c1/8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20/zoom-001.mp4",
"title": "",
"court": "court4",
"duration_s": 9.0
}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 403 | bad_key | The object key does not start with the token club id or contains .. |
| 400 | no_player | Neither user_id nor phone given |
| 409 | clip_not_stored | The video for this key is not in storage yet |
| 404 | player_not_found: … | Player not found (the id or phone follows in the text) |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"ok": true, "user_id": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83"}Live court score #
/v1/ingest/liveAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Current court score for scoreboards and streams. Kept for 180 seconds and expires by itself unless updated. Read with the public GET /v1/live/{club_id}.
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 |
score | string | no | Score line for the scoreboard |
teams | string | no | Line-up line |
set_no | integer, or null | no | Set number |
on_air | boolean | no | Whether the court video stream is on air default false |
Example request
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
}'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, "ttl_s": 180}Current americano game on a court #
/v1/ingest/quick-gamesAccess: Club token Header X-Club-Token: <club token> — the club court server token.
The game of the current americano or mexicano round assigned to this court and still without a score. No such game — {"game": null}.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
court | string | yes | Court name as on the court server |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Example request
curl -X GET "https://your-server/api/v1/ingest/quick-games?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
{
"game": {
"gameId": 17,
"qtId": "6e2b9f41-c7a0-4d3e-9b15-8f4c2a7d0e93",
"round": 2,
"points": 24,
"format": "americano",
"team1": [
{"userId": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83", "name": "Ivan P."},
{"userId": null, "name": "Guest"}
],
"team2": [
{"userId": "a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15", "name": "Anna S."},
{"userId": null, "name": "Oleg"}
]
}
}Americano game score from the court #
/v1/ingest/quick-games/{game_id}/scoreAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Americano game score from the court server (rallies from machine-vision cameras). Only a game of the token's club. Returns the full current state of the americano.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
game_id | integer | yes | gameId from GET /v1/ingest/quick-games |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
score1 | integer | yes | Team 1 points, 0–64 min 0; max 64 |
score2 | integer | yes | Team 2 points, 0–64 min 0; max 64 |
Example request
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}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success |
| 401 | bad_club_token | Invalid club token |
| 404 | game_not_found | Game not found or belongs to another club |
| 409 | finished | The americano is already finished |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Bracket match on a court #
/v1/ingest/bracketAccess: Club token Header X-Club-Token: <club token> — the club court server token.
The next unplayed bracket match of a live club tournament assigned to this court. None — {"match": null}.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
court | string | yes | Court name as on the court server |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Example request
curl -X GET "https://your-server/api/v1/ingest/bracket?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
{
"match": {
"matchId": "8c0f4a52-2d1e-4b7a-9f63-1a5e7d9b3c20",
"tournamentId": "6e2b9f41-c7a0-4d3e-9b15-8f4c2a7d0e93",
"title": "Club cup",
"round": 1,
"startAt": "2026-10-04T12:00:00",
"team1": [
{"userId": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83", "name": "Ivan P."},
{"userId": null, "name": "Guest"}
],
"team2": [
{"userId": "a52e7c14-90bd-4e3f-8c61-7d2f0b9e4a15", "name": "Anna S."},
{"userId": null, "name": "Guest"}
]
}
}Bracket match result from the court #
/v1/ingest/bracket/{match_id}/resultAccess: Club token Header X-Club-Token: <club token> — the club court server token.
Bracket match result from automatic scoring. A repeat after a connection drop is safe: if the winner is already recorded, the response is {"ok": true, "already": true} and nothing changes.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
match_id | string (uuid) | yes | Match id |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
score | string | yes | Score by sets “6:4,3:6,7:5” pattern ^\d+:\d+(,\d+:\d+)*$ |
winner | integer | yes | Winning team: 1 or 2 min 1; max 2 |
Example request
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}'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}Player levels for the terminal #
/v1/ingest/levelsAccess: Club token Header X-Club-Token: <club token> — the club court server token.
AISOLUS rating of line-up players, shown next to the name on the court terminal. Up to 16 ids and up to 16 phones, comma-separated. The response is a map where the same level is stored under both the id and the phone; players without a rating are omitted. A number that lost its “+” in the URL is restored to “+7…”.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
ids | string | no | Player ids, comma-separated, up to 16 |
phones | string | no | Phones, comma-separated, 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/ingest/levels?ids=3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83&phones=%2B79000000001" \
-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
{
"levels": {
"3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83": {"value": 3.25, "step": "<step name>", "games": 14},
"+79000000001": {"value": 3.25, "step": "<step name>", "games": 14}
}
}Check a player pass in the cloud #
/v1/ingest/player-passAccess: Club token Header X-Club-Token: <club token> — the club court server token.
The court terminal could not verify a player QR pass itself (no day key in the license) — the cloud checks it. Only the id and a short name are returned.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
x-club-token | string | yes | Court server club token |
Request body application/json
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Code from the player's QR pass length up to 120 |
Example request
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>"}'Responses
| Code | detail | Meaning |
|---|---|---|
| 200 | — | Success; example below |
| 401 | bad_club_token | Invalid club token |
| 404 | pass_not_found | Pass not recognized |
| 410 | pass_expired | The pass has expired |
| 422 | […] | Parameter or body validation error: detail is a list of errors |
Example response
{"userId": "3d6e9a10-5b2c-4f8e-a1d7-6c4b2e0f9a83", "name": "Ivan P."}