Skip to content
Developers
OpenAPI
GET /api/v1/races/{raceId}/livescope: tracking:read

Race live

API v1 Last updated 2026-10-01 View as Markdown

#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.