# Race live

# 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](/docs/api/events-live); unknown, malformed, or another club's ⇒ `404 RACE_NOT_FOUND` |

#### Request

```bash
curl -s https://sail-club-server.cloud.run/api/v1/races/9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b/live \
  -H "Authorization: Bearer $TOKEN"
```

```javascript
const res = await fetch(`https://sail-club-server.cloud.run/api/v1/races/${raceId}/live`, {
  headers: { Authorization: `Bearer ${token}` },
});
const live = await res.json();
```

```python
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()
```

```csharp
var res = await client.GetAsync($"/api/v1/races/{raceId}/live");
res.EnsureSuccessStatusCode();
var live = await res.Content.ReadFromJsonAsync<PublicRaceLiveDto>();
```

```php
<?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

```json
{
  "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`](/docs/errors#RACE_NOT_FOUND) |
| 404 | [`TRACKING_NOT_SHARED`](/docs/errors#TRACKING_NOT_SHARED) |
| 401 | [`API_TOKEN_MISSING`](/docs/errors#API_TOKEN_MISSING) / [`API_TOKEN_INVALID`](/docs/errors#API_TOKEN_INVALID) |
| 403 | [`API_SCOPE_MISSING`](/docs/errors#API_SCOPE_MISSING) / [`API_IP_NOT_ALLOWED`](/docs/errors#API_IP_NOT_ALLOWED) |
| 429 | [`RATE_LIMIT_EXCEEDED`](/docs/errors#RATE_LIMIT_EXCEEDED) |
| 503 | [`API_UNAVAILABLE`](/docs/errors#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](/docs/rate-limits)
for the 120/min tracking bucket this call shares with the other three `tracking:read` endpoints.

```prompt
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.
```
