# Race course

# Race course

The course actually applied to each of a race's start groups — the marks and lines an overlay draws under the
boats. Identical shape to the `courses[]` field on [Race results](/docs/api/race-results); this is the same
data as its own call, usable before the race finishes (and without `results:read`).

Unlike the other three `tracking:read` endpoints, the delay does **not** apply here — a course doesn't change
every second, so there's nothing to hold back.

## 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/course \
  -H "Authorization: Bearer $TOKEN"
```

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

```python
res = requests.get(
    f"https://sail-club-server.cloud.run/api/v1/races/{race_id}/course",
    headers={"Authorization": f"Bearer {token}"},
    timeout=10,
)
course = res.json()
```

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

```php
<?php
$ch = curl_init("https://sail-club-server.cloud.run/api/v1/races/$raceId/course");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
$course = json_decode(curl_exec($ch), true);
```

#### Response

```json
{
  "raceId": "9a1c0d2b-7e4a-46f8-9c1d-2e3f9a1c0d2b",
  "startGroups": [
    {
      "startGroupId": "2b6a9c4e-7f10-4a1c-8d2b-7e4a6f8c1d2e",
      "name": "Start A",
      "courseVersion": 3,
      "course": {
        "marks": [{ "key": "M1", "label": "Windward", "lat": 40.99, "lon": 28.79 }],
        "startLine": { "type": "LineSegment", "ends": [{ "lat": 40.97, "lon": 28.78 }, { "lat": 40.971, "lon": 28.781 }] },
        "finishLine": { "type": "LineSegment", "ends": [{ "lat": 40.97, "lon": 28.78 }, { "lat": 40.971, "lon": 28.781 }] },
        "route": ["START", "M1", "FIN"]
      }
    }
  ]
}
```

`200` — same caching headers as every data endpoint, a shared 5-second server-side cache per race.

## Response fields

| Field | Type | Null? | Note |
|---|---|---|---|
| `raceId` | uuid | no | |
| `startGroups[]` | array | no | one entry per start group of this race |
| `startGroups[].startGroupId` / `name` | uuid / string | no | matches [Event live status](/docs/api/events-live) |
| `startGroups[].courseVersion` | int | no | bumped every time the applied course changes — refetch when it differs from the value you last saw |
| `startGroups[].course` | object | **yes** | `null` until a course with placed marks has been applied to this start group. Full field table: [Data models → Race results](/docs/data-models#race-results) (same `PublicCourseDto` shape) |

## 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 entry per `raceId`, refreshed every 5 seconds. Compare `courseVersion` per start group before
redrawing marks — most polls will see no change at all.

```prompt
Implement getRaceCourse(raceId) for the Sail Club API.

GET https://sail-club-server.cloud.run/api/v1/races/{raceId}/course
Authorization: Bearer <access_token>
Scope required: tracking:read

Success 200: { raceId, startGroups: [ { startGroupId, name, courseVersion, course: PublicCourseDto | null } ] }
  — course is the same shape as race-results' courses[] (marks, startLine, finishLine, route). 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.

No delay applies to this endpoint. Only redraw marks/lines when courseVersion changes from what you last
stored for that startGroupId.

Adapt to your actual stack — the endpoint, path param and response shape above are language-neutral.
```
