Build a live broadcast overlay
#Build a live broadcast overlay
The tracking:read endpoints — Event live status, Race live,
Track, Course and Race live insights —
are designed to be polled together to drive a live map overlay for a broadcast or stream. This page is the
assembly instructions; each endpoint's own page has its full field reference.
#Before you call anything
tracking:read is necessary but not sufficient. The event's organiser must also have turned on Share live
tracking with API partners for that specific event (panel: Advanced settings → Mobile broadcast & tracking).
Off (the default) ⇒ every call below answers 404 TRACKING_NOT_SHARED, scope or no scope — this is not
something your credential can work around, and it is not a bug to retry past. The organiser also chooses a
delay (0–600 seconds, default 0): nothing any of these four endpoints returns is ever newer than
now − delaySeconds. Build your UI to show that number ("live, delayed ~30s") rather than claiming "live" with
no qualification.
#The polling loop
- Poll Event live status on a slow timer (every 5–10 s) to discover which races are
currently
isLiveand which start groups they have. This call is cheap — one shared 5-second cache entry per event regardless of how many overlays poll it. - For each race you're actually showing, poll Race live every 1–2 seconds for boat positions, wind, and the unofficial live order. The server-side cache is 2 seconds, so polling faster than that only spends rate-limit budget on an unchanged body.
- Poll Course every 5–10 s (or whenever a race's
courseVersionfrom step 1/2 changes) to redraw marks and lines — it almost never changes mid-race. - If you want a trail behind each boat (not just the newest dot), separately page through
Track with
since= the last point you already have, advancing to each response'snextSince. - If your overlay shows a commentary ticker, poll Race live insights every
15–60 s with
If-None-Matchfor each live race — it's generated at most once a minute server-side, so polling faster than that only re-fetches the same lines.
every 5-10s: GET /events/{eventId}/live -> which races/start groups are live, courseVersion
every 1-2s: GET /races/{raceId}/live -> boat positions, wind, unofficial order (per live race)
on courseVersion change: GET /races/{raceId}/course
every 15-60s: GET /races/{raceId}/live/insights -> commentary lines (per live race, optional)
optional trail: GET /races/{raceId}/track?startGroupId=&since=<last nextSince>#ETag and If-None-Match
Every call returns ETag, Cache-Control: private, max-age=<n>, Last-Modified and X-Data-As-Of like any
other /api/v1 response (see Caching & freshness). Send If-None-Match on every repeat poll
— a 304 with no body is cheaper for both sides than re-parsing an unchanged position list, even though it
still counts against your rate limit the same as a 200 would.
#Rate limits
All five endpoints share one 120 requests/minute per client bucket — separate from, and not shared with, the general 60/min bucket the events/results endpoints use. See Rate limits for the exact numbers. One race polled every 1–2 seconds plus its course, insights and the event summary comfortably fits inside 120/min; polling many races from one client at once is the way you'd actually exceed it — space those calls out or request a higher per-club limit if you run a multi-event broadcast operation.
#Commentary lines
Race live insights turns the same underlying data into short sentences — lead
changes, mark roundings, gains, finishes, wind shifts — already phrased in the language you ask for, meant for a
ticker alongside the map rather than for driving the map itself. It's generated at most once a minute per race,
shared across every client and language, so poll it on its own slow timer (15–60 s) separate from the 1–2 s
position poll. Each line keeps a stable id — track which ids you've already shown so a re-fetch of the same
60-second window doesn't redraw your ticker. If you'd rather build your own wording than use the server's
pre-rendered text, use kind plus the numeric/boat fields instead — see that page's field reference.
#Replaying a finished race from Track
Track keeps answering after the race finishes — there's no separate "replay" endpoint and no extra retention window beyond what the live system already keeps. To build a post-race highlights replay:
- Call Event live status (or your own earlier record) for the start group's
startTimeUtc/finishTimeUtc. - Page Track with
since=startTimeUtc(orstartTimeUtc − 5 minto include the pre-start), advancingsinceto each response'snextSinceuntil it comes backnull. - Each 10-minute page you fetch once the race is well over is
max-age=3600(effectively immutable) — cache it on your side permanently; you'll never need to re-fetch that window. - Feed the concatenated
t/lat/lonarrays per boat into whatever timeline scrubber your overlay uses — the data is exactly the same shape whether you're drawing it live or replaying it an hour later.
Prompt for AI agents ready to paste into an agent
Build a live broadcast overlay client for the Sail Club API (tracking:read scope).
Endpoints (base https://sail-club-server.cloud.run, Authorization: Bearer <access_token> on every call):
GET /api/v1/events/{eventId}/live -> races/start groups live now, courseVersion (poll every 5-10s)
GET /api/v1/races/{raceId}/live -> boat positions, wind, unofficial order (poll every 1-2s per live race)
GET /api/v1/races/{raceId}/course -> marks/lines, courseVersion (poll every 5-10s, or on version change)
GET /api/v1/races/{raceId}/live/insights?lang= -> commentary lines (kind + text) (poll every 15-60s per live race, optional)
GET /api/v1/races/{raceId}/track?startGroupId=&since=&until= -> 1Hz trail / replay, page via nextSince
All five share one rate-limit bucket: 120 requests/minute per client (see /docs/rate-limits), separate from the
60/min bucket other endpoints use. Store the ETag per URL and send If-None-Match on every repeat poll; a 304
means keep your cached copy. 404 TRACKING_NOT_SHARED means the event's organiser hasn't opted in for this event
-- stop polling that event, don't retry. Every response carries delaySeconds and dataAsOf -- nothing returned is
newer than dataAsOf; show the delay in your UI rather than labeling the feed "live" unqualified.
Build: (1) a poller that fetches event live status on a slow timer and race live + course on a fast timer for
each isLive race, redrawing boat markers keyed by entryId; (2) an insights poller on a 15-60s timer per live
race, keyed by insight id so a line is only shown once; (3) a Track-based replay mode that pages from a
race's startTimeUtc to nextSince === null and feeds the result into a scrubber; (4) ETag-aware caching so a 304
never triggers a redraw.
Adapt to your actual stack -- the endpoints, cadence and caching rules above are language-neutral.