Track
#Track
The raw ~1 Hz position history of one start group — the same points Race live only ever
shows the newest of. Use it to draw a trail behind each boat while racing, or to replay the whole race
afterwards; the capture window (gun − 5 min … gun + 3 h, or each boat's own finish) and the opt-in rule are the
same as every other tracking:read call, and this endpoint keeps answering after the race is over.
#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 |
#Query parameters
| Param | Type | Required | Note |
|---|---|---|---|
startGroupId |
UUID | only if the race has more than one start group | the group to read; omit it for a single-start-group race |
since |
date-time (ISO 8601 UTC) | yes | inclusive lower bound |
until |
date-time (ISO 8601 UTC) | no | exclusive upper bound; default and max is since + 10 min; must be after since |
until is silently cut at now − delaySeconds if your requested value would reach past it — read the
response's own until back, don't assume you got what you asked for.
#Request
curl -s "https://sail-club-server.cloud.run/api/v1/races/9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b/track?startGroupId=2b6a9c4e-7f10-4a1c-8d2b-7e4a6f8c1d2e&since=2027-05-01T11:40:00Z" \
-H "Authorization: Bearer $TOKEN"const url = new URL(`https://sail-club-server.cloud.run/api/v1/races/${raceId}/track`);
url.searchParams.set('startGroupId', startGroupId);
url.searchParams.set('since', sinceIso);
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const page = await res.json();res = requests.get(
f"https://sail-club-server.cloud.run/api/v1/races/{race_id}/track",
params={"startGroupId": start_group_id, "since": since_iso},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
page = res.json()var res = await client.GetAsync($"/api/v1/races/{raceId}/track?startGroupId={startGroupId}&since={sinceIso}");
res.EnsureSuccessStatusCode();
var page = await res.Content.ReadFromJsonAsync<PublicTrackPageDto>();<?php
$query = http_build_query(["startGroupId" => $startGroupId, "since" => $sinceIso]);
$ch = curl_init("https://sail-club-server.cloud.run/api/v1/races/$raceId/track?$query");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
$page = json_decode(curl_exec($ch), true);#Response
{
"raceId": "9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b",
"startGroupId": "2b6a9c4e-7f10-4a1c-8d2b-7e4a6f8c1d2e",
"since": "2027-05-01T11:40:00Z",
"until": "2027-05-01T11:50:00Z",
"nextSince": "2027-05-01T11:50:00Z",
"delaySeconds": 30,
"boats": [
{
"entryId": "e3f9a1c0-d2b7-4e4a-96f8-c1d2e3f9a1c0",
"sailNumber": "TUR 1",
"boatName": "Poyraz",
"t": [1935654000000, 1935654001000],
"lat": [40.97, 40.9701],
"lon": [28.78, 28.7801],
"sogKn": [6.1, null],
"cogDeg": [215.0, 214.8]
}
]
}200 — a window ending at least 2 minutes before the current cutoff is Cache-Control: max-age=3600 (it's
immutable, that minute of track never changes again); a window that still reaches close to "now" is max-age=2.
#Response fields
| Field | Type | Null? | Note |
|---|---|---|---|
since |
date-time | no | echoes what you requested |
until |
date-time | no | the requested until, cut at now − delaySeconds if that was earlier — always after since |
nextSince |
date-time | yes | pass this straight back as since on your next call to keep paging forward; null once the start group's capture window (gun + 3 h) has closed — stop polling this group. For a group with no gun time yet, equals until |
boats[] |
array | no (may be empty) | only boats with ≥ 1 point inside [since, until), by sail number |
boats[].entryId / sailNumber / boatName |
uuid / string / string | no | same id/fields as Race live |
boats[].t |
long[] | no | epoch milliseconds, strictly ascending, ~1 Hz; one entry per sample |
boats[].lat / lon |
double[] | no | WGS84, same index as t |
boats[].sogKn / cogDeg |
(double|null)[] | elements nullable | same index as t; null where the phone didn't report it for that sample |
Every per-boat array (t, lat, lon, sogKn, cogDeg) is the same length and index-aligned — t[i]/lat[i]/
lon[i] are one position fix.
#Errors
| HTTP | Code |
|---|---|
| 400 | API_PARAM_INVALID — params.name is startGroupId, since, or until |
| 404 | RACE_NOT_FOUND |
| 404 | START_GROUP_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
Each 10-minute-or-smaller window is its own cache entry. Once a window ends more than 2 minutes before the
current delayed cutoff it never changes again — cache it forever on your side too, keyed by
raceId + startGroupId + since + until.
Prompt for AI agents ready to paste into an agent
Implement getTrackPage(raceId, startGroupId, since, until?) for the Sail Club API.
GET https://sail-club-server.cloud.run/api/v1/races/{raceId}/track?startGroupId=&since=&until=
Authorization: Bearer <access_token>
Scope required: tracking:read
Success 200: { raceId, startGroupId, since, until, nextSince, delaySeconds,
boats: [ { entryId, sailNumber, boatName, t: [epoch_ms...], lat: [...], lon: [...], sogKn: [...|null],
cogDeg: [...|null] } ] } — arrays are index-aligned per boat. Fetch GET /api/v1/openapi.json for the exact
schema.
400 API_PARAM_INVALID (params.name: startGroupId|since|until): fix that parameter, don't retry as-is.
404 RACE_NOT_FOUND / START_GROUP_NOT_FOUND: 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.
Page forward by calling again with since = the previous response's nextSince, stopping when nextSince is null
(the capture window has closed). Cache each window permanently once its own until is more than 2 minutes behind
"now minus delaySeconds" — it will never change again.
Adapt to your actual stack — the endpoint, path param and response shape above are language-neutral.