# Build a live broadcast overlay

# Build a live broadcast overlay

The `tracking:read` endpoints — [Event live status](/docs/api/events-live), [Race live](/docs/api/races-live),
[Track](/docs/api/races-track), [Course](/docs/api/races-course) and [Race live insights](/docs/api/races-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](/docs/api/events-live) 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](/docs/api/races-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](/docs/api/races-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](/docs/api/races-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](/docs/api/races-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.

```text
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](/docs/caching)). 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](/docs/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](/docs/api/races-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](/docs/api/races-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](/docs/api/events-live) (or your own earlier record) for the start group's
   `startTimeUtc`/`finishTimeUtc`.
2. Page [Track](/docs/api/races-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
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.
```
