Race live
#Race live
The newest reported position (and heading/speed) per boat per start group, the latest committee wind reading,
and the unofficial live order — everything a broadcast overlay redraws every poll. Nothing in the body is newer
than dataAsOf, which itself is now − delaySeconds.
#Headers
| Header | Required | Value |
|---|---|---|
Authorization |
yes | Bearer <access_token> |
#Path parameters
| Param | Type | Note |
|---|---|---|
raceId |
UUID | from Event live status; unknown, malformed, or another club's ⇒ 404 RACE_NOT_FOUND |
#Request
curl -s https://sail-club-server.cloud.run/api/v1/races/9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b/live \
-H "Authorization: Bearer $TOKEN"const res = await fetch(`https://sail-club-server.cloud.run/api/v1/races/${raceId}/live`, {
headers: { Authorization: `Bearer ${token}` },
});
const live = await res.json();res = requests.get(
f"https://sail-club-server.cloud.run/api/v1/races/{race_id}/live",
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
live = res.json()var res = await client.GetAsync($"/api/v1/races/{raceId}/live");
res.EnsureSuccessStatusCode();
var live = await res.Content.ReadFromJsonAsync<PublicRaceLiveDto>();<?php
$ch = curl_init("https://sail-club-server.cloud.run/api/v1/races/$raceId/live");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
$live = json_decode(curl_exec($ch), true);#Response
{
"raceId": "9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b",
"eventId": "1143117a-9c2b-4e7a-8f1d-2b6a9c4e7f10",
"raceNo": 1,
"name": "Race 1",
"isLive": true,
"delaySeconds": 30,
"dataAsOf": "2027-05-01T11:59:30.000Z",
"startGroups": [
{
"startGroupId": "2b6a9c4e-7f10-4a1c-8d2b-7e4a6f8c1d2e",
"name": "Start A",
"state": "Started",
"startTimeUtc": "2027-05-01T11:30:00Z",
"finishTimeUtc": null,
"courseVersion": 3,
"wind": { "twd": 250, "tws": 12.0, "measuredAtUtc": "2027-05-01T11:50:00Z" },
"boats": [
{ "entryId": "e3f9a1c0-d2b7-4e4a-96f8-c1d2e3f9a1c0", "sailNumber": "TUR 1", "boatName": "Poyraz",
"lat": 40.97, "lon": 28.78, "sogKn": 6.1, "cogDeg": 215.0, "fixTimeUtc": "2027-05-01T11:59:29Z" }
],
"liveOrder": [
{ "entryId": "e3f9a1c0-d2b7-4e4a-96f8-c1d2e3f9a1c0", "rank": 1, "elapsedSeconds": 1700,
"correctedSeconds": 1650, "deltaSeconds": 0, "finishTimeUtc": null }
],
"liveOrderAsOf": "2027-05-01T11:59:20Z"
}
]
}200 — same caching headers as every data endpoint, a shared 2-second server-side cache per race.
#Response fields
| Field | Type | Null? | Note |
|---|---|---|---|
dataAsOf |
date-time | no | now − delaySeconds at the shared read; also X-Data-As-Of — nothing in the body is newer than this |
startGroups[].wind |
object | yes | newest committee reading at or before dataAsOf; twd 0–359°, tws knots (null = direction only), measuredAtUtc |
startGroups[].boats[] |
array | no (may be empty) | approved entries with a fix inside the capture window, by sail number — no fix, no phone, or outside the window ⇒ absent from the array entirely |
boats[].entryId |
uuid | no | same id the results endpoints use |
boats[].sailNumber / boatName |
string | no | as entered on the entry |
boats[].lat / lon |
double | no | WGS84 |
boats[].sogKn / cogDeg |
double | yes | null when the phone didn't report it for this fix |
boats[].fixTimeUtc |
date-time | no | the phone's own sample time, always ≤ dataAsOf |
startGroups[].liveOrder[] |
array | yes | null while the race isn't live, or no order read exists yet at or before dataAsOf — a delayed feed shows it delaySeconds later than a live viewer would. Sorted by rank (unranked last). Unofficial — never cite as a result |
liveOrder[].rank |
int | yes | 1 = currently leading |
liveOrder[].elapsedSeconds / correctedSeconds / deltaSeconds |
double | yes | as the live leaderboard computed them at that moment |
liveOrder[].finishTimeUtc |
date-time | yes | phone-recorded finish, only once ≤ dataAsOf |
startGroups[].liveOrderAsOf |
date-time | yes | null together with liveOrder |
No user ids, no GPS accuracy, no device data beyond position/speed/heading — same privacy floor as the app's own live screen.
#Errors
| HTTP | Code |
|---|---|
| 404 | RACE_NOT_FOUND |
| 404 | TRACKING_NOT_SHARED |
| 401 | API_TOKEN_MISSING / API_TOKEN_INVALID |
| 403 | API_SCOPE_MISSING / API_IP_NOT_ALLOWED |
| 429 | RATE_LIMIT_EXCEEDED |
| 503 | API_UNAVAILABLE |
#Caching
One shared cache entry per raceId, refreshed every 2 seconds regardless of how many clients poll it — polling
faster than that just spends your rate-limit budget on an unchanged body. See Rate limits
for the 120/min tracking bucket this call shares with the other three tracking:read endpoints.
Prompt for AI agents ready to paste into an agent
Implement getRaceLive(raceId) for the Sail Club API.
GET https://sail-club-server.cloud.run/api/v1/races/{raceId}/live
Authorization: Bearer <access_token>
Scope required: tracking:read
Success 200: { raceId, eventId, raceNo, name, isLive, delaySeconds, dataAsOf,
startGroups: [ { startGroupId, name, state, startTimeUtc, finishTimeUtc, courseVersion,
wind: { twd, tws, measuredAtUtc } | null,
boats: [ { entryId, sailNumber, boatName, lat, lon, sogKn, cogDeg, fixTimeUtc } ],
liveOrder: [ { entryId, rank, elapsedSeconds, correctedSeconds, deltaSeconds, finishTimeUtc } ] | null,
liveOrderAsOf } ] } — fetch GET /api/v1/openapi.json for the exact schema.
404 RACE_NOT_FOUND: unknown/malformed/another club's race id — not retryable.
404 TRACKING_NOT_SHARED: sharing is off for this event — not retryable, needs an organiser change.
401/403/429/503: same handling as every other call — see the full-integration prompt on /docs/overview.
liveOrder is unofficial — label it as such in any UI, never present it as a result. Poll no faster than every
2 seconds (the server-side cache window); redraw boat markers from boats[] keyed by entryId.
Adapt to your actual stack — the endpoint, path param and response shape above are language-neutral.