# Data models

# Data models

Every response is exactly the DTO the equivalent public results page already renders — nothing added, and
personal fields (user ids, emails, phone numbers) are never included. This page documents the fields you'll
actually use; the definitive, always-current schema is
[`GET /api/v1/openapi.json`](/api/v1/openapi.json) — generate your types from it directly if your stack
supports that (e.g. `openapi-typescript`, `openapi-python-client`, NSwag for C#).

## Event summary

Returned by [List events](/docs/api/events) (`items[]`).

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `id` | string (UUID) | no | use as `eventId` on every other endpoint |
| `name` | string | no | |
| `location` | string | no | free-text venue |
| `startDate` / `endDate` | string (ISO 8601 UTC) | no | |
| `status` | string | no | one of `Canceled`, `Completed`, `Registration Open`, `Live`, `Upcoming` (computed server-side from the event's dates and registration window, in that priority order — treat an unrecognized value as `Upcoming` for forward compatibility) |
| `coverUrl` | string (URL) | yes | falls back to the poster image server-side; never null just because there's no dedicated cover |
| `posterUrl` | string (URL) | yes | |
| `entryCount` | number | no | |

## Event detail

Returned by [Get event](/docs/api/event). Everything from **Event summary** above, plus:

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `logoUrl` | string (URL) | yes | |
| `description` | string | yes | |
| `registrationStart` / `registrationEnd` | string (ISO 8601) | yes | |
| `website`, `noticeOfRaceUrl`, `resultsUrl`, `liveTrackUrl`, `galleryUrl` | string (URL) | yes | external links the event set |
| `externalRegistrationFormUrl`, `externalPaymentUrl` | string (URL) | yes | |
| `instagram`, `whatsapp` | string (URL) | yes | the event's own public contact links, not a person's |
| `isOfficial` | boolean | yes | |
| `type` | string | yes | e.g. `Regatta`, `Series` |
| `classes` | string[] | yes | boat classes racing |
| `organizerId` / `organizer` | string / object | yes | the main organising club |
| `coOrganizers` | array | no (`[]`) | co-host clubs — an event is visible to a co-host's API clients too |
| `divisions`, `specialCategories`, `discardRules` | array | yes | see below |
| `raceOfficeEnabled` | boolean | yes | optional-on-purpose — check `=== true`, never `!== false` |
| `races` | array | no (`[]`) | same shape as **Race summary** below |

## Race summary

Returned by [List races](/docs/api/races) and as `races[]` on **Event detail**.

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `id` | string (UUID) | no | use as `raceId` on [Race results](/docs/api/race-results) |
| `name` | string | no | |
| `scheduledDate` | string (ISO 8601) | no | |
| `status` | string | yes | free text set by race management, not a closed enum — values seen today: `Scheduled`, `OnGoing`, `Completed`, `Cancelled` (double-L; the event `status` above uses `Canceled`, single-L — two different fields, two different spellings, both intentional). Treat as display text and drive logic off `hasResults` instead. |
| `hasResults` | boolean | no | call race results only once this is `true` |
| `isOfficial` | boolean | yes | |
| `resultCount` | number | no | |
| `raceNo` | number | yes | sequence number within the event |
| `raceType` | string | yes | e.g. `Fleet`, `Pursuit` |
| `boatModel` | string | yes | set for one-design races |

## Race results

Returned by [Race results](/docs/api/race-results).

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `id`, `name`, `eventId`, `eventName`, `scheduledDate` | — | no | race identity |
| `courseDistanceNm` | number | yes | nautical miles |
| `resultsUpdatedAtUtc` | string (ISO 8601) | yes | |
| `results` | array | no (`[]`) | one row per boat, see below |

### Race result row

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `rank` | number | no | |
| `sailNumber`, `boatName` | string | no | |
| `teamName`, `skipper`, `owner` | string | yes | entered names, not user accounts |
| `className`, `divisionName`, `divisionColorHexCode`, `overallGroupName`, `modelClass` | string | yes | |
| `status` | string | no | `Finished` or a scoring code — see **Scoring vocabulary** below |
| `startTimeUtc`, `finishTimeUtc` | string (ISO 8601) | yes | absent for a non-finishing boat |
| `elapsedTime`, `correctedTime` | string (`HH:MM:SS`) | yes | corrected time is after rating adjustment |
| `raiting`, `raitingLabel` | number / string | yes | the boat's rating factor used for this race (sic — matches the wire field name) |
| `points` | number | no | scoring points for this race |
| `penaltyPoints`, `timePenaltySeconds` | number | yes | set when a redress/penalty applied |

## Event results

Returned by [Event results](/docs/api/event-results) — four lists, all the same shape family.

| List | Row shape | Meaning |
|---|---|---|
| `results` | scoring result | per-division series standings |
| `overall` | overall result | combined standings across divisions in one overall group |
| `ratingGroups` | scoring result + `ratingGroup: string` | standings within one rating band (e.g. an IRC class split) |
| `specialCategories` | special-category result | standings for an award category (e.g. "Best Corinthian") |

### Scoring / overall result row

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `rank`, `sailNumber`, `boatName` | — | no | |
| `skipper`, `owner`, `teamName`, `modelClass` | string | yes | |
| `divisionName` | string | yes | absent on `overall` rows |
| `score` | number | no | total after discards |
| `gross` | number | yes | total before discards, when different |
| `points` | array | no (`[]`) | one entry per race — see below |

### Points entry (`points[]`, every list)

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `raceNo` | number | no | |
| `score` | number | no | this race's contribution |
| `isDiscarded` | boolean | no | dropped under the event's discard rule — excluded from `score`/`gross` when `true` |
| `penalty`, `penaltyPoints`, `timePenaltySeconds` | — | yes | |
| `isPlaceholder` | boolean | yes | a race the boat is scored for but that hasn't happened/been entered yet |

### Special-category result row

| Field | Type | Null? | Meaning |
|---|---|---|---|
| `categoryName` | string | no | |
| `rank`, `sailNumber`, `boatName` | — | no | |
| `skipper`, `owner`, `teamName`, `modelClass` | string | yes | |
| `totalScore` | number | no | total after discards — same concept as `score` on the scoring/overall/ratingGroups rows above; special-category rows name it `totalScore` instead (sic — matches the wire field name; not worth a breaking rename) |
| `points` | array | no (`[]`) | same shape as above |

## Scoring vocabulary

For someone who hasn't seen sailboat racing scoring before:

- **Corrected time** — a boat's elapsed time adjusted by its rating (handicap), so boats of different speed
  potential can be compared fairly on one results list. Ranking within a race is by corrected time, not raw
  elapsed time.
- **Discard** — most multi-race series let a sailor drop their worst race(s) from the total; a discarded
  race still appears in `points[]` (with `isDiscarded: true`) but doesn't count toward `score`.
- **DNF** (Did Not Finish), **DNS** (Did Not Start), **OCS** (On Course Side — started early), **RET**
  (Retired), **DSQ** (Disqualified) — status codes in place of a finish time; each carries its own scoring
  penalty under the event's rules (typically "finishers + 1" or similar), reflected in `points`/`score`
  rather than a time.
- **Rating** (`raiting`/`raitingLabel`) — the handicap factor applied to a boat's elapsed time to produce its
  corrected time; lower is generally faster-rated.
