Skip to content
Developers
OpenAPI

Build a live broadcast overlay

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

#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

  1. Poll Event live status on a slow timer (every 5–10 s) to discover which races are currently isLive and which start groups they have. This call is cheap — one shared 5-second cache entry per event regardless of how many overlays poll it.
  2. 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.
  3. Poll Course every 5–10 s (or whenever a race's courseVersion from step 1/2 changes) to redraw marks and lines — it almost never changes mid-race.
  4. 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's nextSince.
  5. If your overlay shows a commentary ticker, poll Race live insights every 15–60 s with If-None-Match for 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:

  1. Call Event live status (or your own earlier record) for the start group's startTimeUtc/finishTimeUtc.
  2. Page Track with since = startTimeUtc (or startTimeUtc − 5 min to include the pre-start), advancing since to each response's nextSince until it comes back null.
  3. 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.
  4. Feed the concatenated t/lat/lon arrays 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.