{
  "openapi": "3.1.0",
  "info": {
    "title": "Sail Club public API",
    "version": "1.0.0",
    "summary": "Read a club\u0027s published regattas, races and results, server to server.",
    "description": "Read-only API for a club\u0027s own published sailing events. A club admin creates an API client in the Sail Club panel (API access) and gives the integrator its client_id and client_secret. The integrator\u0027s SERVER exchanges them for a 1-hour Bearer token (OAuth 2.0 client credentials) and calls the data endpoints. There is no clubId parameter: the token decides the club, and only events that club organises or co-organises and that are published are visible. Browser (CORS) calls are not supported \u2014 keep the secret on your server.\n\nFreshness: responses are cached on our side for up to one hour (Cache-Control: private, max-age=\u003Cseconds left\u003E). Send If-None-Match with the last ETag to get 304 Not Modified. X-Data-As-Of says when the body was read from our read model; Last-Modified says when that content last changed.\n\nErrors: application/problem\u002Bjson with a stable machine \u0060code\u0060 (switch on it, never on text) and optional \u0060params\u0060. The token endpoint answers RFC 6749 JSON ({\u0022error\u0022,\u0022code\u0022})."
  },
  "servers": [
    {
      "url": "https://sail-club-server.cloud.run"
    }
  ],
  "security": [
    {
      "clientCredentials": []
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Get and check an access token."
    },
    {
      "name": "Events",
      "description": "The club\u0027s published events, one event\u0027s detail and its race list (scope events:read)."
    },
    {
      "name": "Results",
      "description": "Race results and event (series) results (scope results:read)."
    }
  ],
  "paths": {
    "/api/v1/oauth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "getToken",
        "summary": "Get an access token (OAuth 2.0 client credentials)",
        "description": "RFC 6749 \u00A74.4. Send the client credentials either as HTTP Basic (\u0060Authorization: Basic base64(urlencode(client_id):urlencode(client_secret))\u0060) or as form fields \u0060client_id\u0060 \u002B \u0060client_secret\u0060 \u2014 not both. Optional \u0060scope\u0060 (space-separated) narrows the token to a subset of the client\u0027s scopes; omitted = all of them. The token is opaque (not a JWT) and valid for \u0060expires_in\u0060 seconds (3600). Cache it and reuse it; fetch a new one shortly before it expires or when a data call answers 401. Rate limit: 20 requests/min per client_id and per IP.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "const": "client_credentials",
                    "description": "Always \u0060client_credentials\u0060."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "The client id (\u0060scc_\u2026\u0060), when not sent as HTTP Basic."
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "The client secret (\u0060scs_\u2026\u0060), when not sent as HTTP Basic. Never log it."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Optional space-separated subset of the client\u0027s scopes, e.g. \u0060events:read results:read\u0060."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued. \u0060Cache-Control: no-store\u0060.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "\u0060invalid_request\u0060 (API_TOKEN_REQUEST_INVALID), \u0060unsupported_grant_type\u0060 (API_GRANT_TYPE_UNSUPPORTED), \u0060unauthorized_client\u0060 (API_CLIENT_DISABLED, API_CLIENT_EXPIRED, API_IP_NOT_ALLOWED) or \u0060invalid_scope\u0060 (API_SCOPE_INVALID, params.scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "\u0060invalid_client\u0060 (API_CLIENT_CREDENTIALS_INVALID): unknown client_id or wrong secret \u2014 same answer for both.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/token-info": {
      "get": {
        "tags": [
          "Auth"
        ],
        "operationId": "getTokenInfo",
        "summary": "Check the current token",
        "description": "Which client and club the token belongs to, its scopes and expiry. Any valid token; no scope needed. Not cached.",
        "responses": {
          "200": {
            "description": "The token\u0027s client, club, scopes and expiry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenInfo"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEvents",
        "summary": "List the club\u0027s published events",
        "description": "The events the token\u0027s club organises (\u0060isCoHost: false\u0060) or co-organises (\u0060isCoHost: true\u0060) and that are published, newest start date first (events without a start date first). Same items as the club\u0027s public event list on sailracing.club. Paged: \u0060page\u0060 (1-based, default 1) and \u0060pageSize\u0060 (1\u2013100, default 20); follow \u0060nextPage\u0060 until it is null. At most 1000 events can be paged. Requires scope \u0060events:read\u0060.",
        "security": [
          {
            "clientCredentials": [
              "events:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Items per page, 1\u2013100 (default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Cached on the server for up to an hour; see the headers.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/Last-Modified"
              },
              "X-Data-As-Of": {
                "$ref": "#/components/headers/X-Data-As-Of"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicEventPage"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "description": "API_PARAM_INVALID (params.name = the parameter): a query parameter is malformed or out of range.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist \u00B7 API_SCOPE_MISSING (params.scope): the token lacks the scope.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/events/{eventId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "getEvent",
        "summary": "Get one event",
        "description": "The event\u0027s full public detail: dates, venue, links, images, divisions, classes open for entry, special categories, discard rules, organisers and the race list (\u0060races\u0060). 404 EVENT_NOT_FOUND when the event does not exist, is not published, or is not organised/co-organised by the token\u0027s club. Requires scope \u0060events:read\u0060.",
        "security": [
          {
            "clientCredentials": [
              "events:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Cached on the server for up to an hour; see the headers.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/Last-Modified"
              },
              "X-Data-As-Of": {
                "$ref": "#/components/headers/X-Data-As-Of"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicEventDetailDto"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist \u00B7 API_SCOPE_MISSING (params.scope): the token lacks the scope.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND: not found, not published, or not the token club\u0027s (never 403 \u2014 existence is not revealed).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/events/{eventId}/races": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEventRaces",
        "summary": "List the event\u0027s races",
        "description": "The event\u0027s races in schedule order \u2014 the same array as \u0060races\u0060 in GET /api/v1/events/{eventId}. Use \u0060id\u0060 with GET /api/v1/races/{raceId}/results; \u0060hasResults\u0060 tells whether results exist yet. Requires scope \u0060events:read\u0060.",
        "security": [
          {
            "clientCredentials": [
              "events:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Cached on the server for up to an hour; see the headers.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/Last-Modified"
              },
              "X-Data-As-Of": {
                "$ref": "#/components/headers/X-Data-As-Of"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PublicRaceItemDto"
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist \u00B7 API_SCOPE_MISSING (params.scope): the token lacks the scope.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND: not found, not published, or not the token club\u0027s (never 403 \u2014 existence is not revealed).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/races/{raceId}/results": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RaceId"
        }
      ],
      "get": {
        "tags": [
          "Results"
        ],
        "operationId": "getRaceResults",
        "summary": "Get one race\u0027s results",
        "description": "The race\u0027s results page: one row per boat (\u0060results\u0060, ordered by division then rank), start groups with their planned start and registered boat count, and the applied course(s). 404 RACE_NOT_FOUND when the race does not exist or its event is not published or not the token club\u0027s. Requires scope \u0060results:read\u0060.",
        "security": [
          {
            "clientCredentials": [
              "results:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Cached on the server for up to an hour; see the headers.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/Last-Modified"
              },
              "X-Data-As-Of": {
                "$ref": "#/components/headers/X-Data-As-Of"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRaceResultPageDto"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist \u00B7 API_SCOPE_MISSING (params.scope): the token lacks the scope.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "RACE_NOT_FOUND: not found, not published, or not the token club\u0027s (never 403 \u2014 existence is not revealed).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/events/{eventId}/results": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "tags": [
          "Results"
        ],
        "operationId": "getEventResults",
        "summary": "Get the event\u0027s (series) results",
        "description": "The four result lists of the event\u0027s public results tab: \u0060results\u0060 (per division), \u0060overall\u0060 (overall groups spanning divisions), \u0060ratingGroups\u0060 (separate IRC / ORC rankings) and \u0060specialCategories\u0060 (e.g. youth, female helm). Each list may be empty. 404 EVENT_NOT_FOUND as for GET /api/v1/events/{eventId}. Requires scope \u0060results:read\u0060.",
        "security": [
          {
            "clientCredentials": [
              "results:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK. Cached on the server for up to an hour; see the headers.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/Last-Modified"
              },
              "X-Data-As-Of": {
                "$ref": "#/components/headers/X-Data-As-Of"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEventResultsDto"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "API_IP_NOT_ALLOWED: the caller IP is not in the client\u0027s allowlist \u00B7 API_SCOPE_MISSING (params.scope): the token lacks the scope.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "EVENT_NOT_FOUND: not found, not published, or not the token club\u0027s (never 403 \u2014 existence is not revealed).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "description": "INTERNAL_ERROR: unexpected server error; \u0060traceId\u0060 identifies it for support.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "tags": [
          "Auth"
        ],
        "operationId": "getOpenApi",
        "summary": "This document",
        "description": "The OpenAPI 3.1 description of the v1 API. Anonymous.",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 JSON."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "clientCredentials": {
        "type": "oauth2",
        "description": "OAuth 2.0 client credentials. POST /api/v1/oauth/token with HTTP Basic (client_id:client_secret) or form fields; use the access_token as \u0060Authorization: Bearer \u003Ctoken\u003E\u0060 until it expires (3600 s). Do not fetch a token per call \u2014 the token endpoint is rate limited.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/api/v1/oauth/token",
            "scopes": {
              "events:read": "The club\u0027s published events, event detail, race list.",
              "results:read": "Race results and event results."
            }
          }
        }
      }
    },
    "headers": {
      "Cache-Control": {
        "description": "\u0060private, max-age=\u003Cn\u003E\u0060: seconds left in our one-hour cache window. Do not cache longer than n.",
        "schema": {
          "type": "string"
        }
      },
      "ETag": {
        "description": "Strong validator of the exact body. Send it back as If-None-Match.",
        "schema": {
          "type": "string"
        }
      },
      "Last-Modified": {
        "description": "HTTP date: when our read model last rebuilt this content (for a list: the newest item).",
        "schema": {
          "type": "string"
        }
      },
      "X-Data-As-Of": {
        "description": "ISO 8601 UTC with milliseconds: when this body was read from our read model (start of the cache window).",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Not Modified \u2014 the If-None-Match ETag still matches; reuse your copy. Same cache headers, no body.",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          },
          "ETag": {
            "$ref": "#/components/headers/ETag"
          },
          "Last-Modified": {
            "$ref": "#/components/headers/Last-Modified"
          },
          "X-Data-As-Of": {
            "$ref": "#/components/headers/X-Data-As-Of"
          }
        }
      },
      "Unauthorized": {
        "description": "API_TOKEN_MISSING (no Authorization header) \u00B7 API_TOKEN_INVALID (unknown, expired, not an API token, or killed by a secret rotation \u2014 get a new token) \u00B7 API_CLIENT_DISABLED \u00B7 API_CLIENT_EXPIRED. \u0060WWW-Authenticate: Bearer realm=\u0022sailclub-api\u0022\u0060.",
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "RATE_LIMIT_EXCEEDED: more than 60 data calls/min for this client (token endpoint: 20/min per client_id and per IP). Wait \u0060Retry-After\u0060 seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unavailable": {
        "description": "API_UNAVAILABLE: our read model did not answer. Nothing was cached; retry after \u0060Retry-After\u0060 seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "parameters": {
      "EventId": {
        "name": "eventId",
        "in": "path",
        "required": true,
        "description": "Event id as returned by GET /api/v1/events (\u0060id\u0060). Ids of events the token\u0027s club does not organise answer 404.",
        "schema": {
          "type": "string"
        }
      },
      "RaceId": {
        "name": "raceId",
        "in": "path",
        "required": true,
        "description": "Race id (UUID) as returned in the event\u0027s race list (\u0060id\u0060).",
        "schema": {
          "type": "string"
        }
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "The ETag of a previous response; unchanged content answers 304 with no body.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "PublicEventListItemDto": {
        "type": "object",
        "description": "One published event in the club\u0027s list.",
        "required": [
          "id",
          "name",
          "location",
          "startDate",
          "endDate",
          "status",
          "coverUrl",
          "entryCount",
          "posterUrl",
          "isCoHost"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id \u2014 use it in /api/v1/events/{eventId}."
          },
          "name": {
            "type": "string",
            "description": "Event name."
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Venue (marina/club), else city; null if unknown."
          },
          "startDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "First day, UTC; null if not set."
          },
          "endDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last day, UTC; null if not set."
          },
          "status": {
            "type": "string",
            "description": "Computed state: e.g. \u0060Upcoming\u0060, \u0060RegistrationOpen\u0060, \u0060Live\u0060, \u0060Completed\u0060, \u0060Canceled\u0060."
          },
          "coverUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cover image URL, else the poster URL (when equal to posterUrl there is no separate cover); null = none."
          },
          "entryCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of entries (boats) registered."
          },
          "posterUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Poster image URL, no fallback; null = none."
          },
          "isCoHost": {
            "type": "boolean",
            "description": "true when the token\u0027s club co-organises the event rather than being its main organiser."
          }
        }
      },
      "PublicEventPage": {
        "type": "object",
        "description": "A page of events.",
        "required": [
          "items",
          "nextPage",
          "totalItems"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicEventListItemDto"
            },
            "description": "The events on this page."
          },
          "nextPage": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Next page number, or null on the last page."
          },
          "totalItems": {
            "type": "integer",
            "format": "int32",
            "description": "Total number of events across all pages."
          }
        }
      },
      "PublicEventDetailDto": {
        "type": "object",
        "description": "An event\u0027s full public detail, as on its public page.",
        "required": [
          "id",
          "name",
          "location",
          "description",
          "startDate",
          "endDate",
          "registrationStart",
          "registrationEnd",
          "status",
          "coverUrl",
          "posterUrl",
          "website",
          "isOfficial",
          "entryCount",
          "countryCount",
          "races",
          "tieBreakStrategy",
          "divisions",
          "specialCategories",
          "discardRules",
          "isAwardsPublished",
          "organizerId",
          "logoUrl",
          "noticeOfRaceUrl",
          "resultsUrl",
          "liveTrackUrl",
          "instagram",
          "whatsapp",
          "rrsEventId",
          "type",
          "classes",
          "externalRegistrationFormUrl",
          "externalPaymentUrl",
          "aiAssistantEnabled",
          "galleryUrl",
          "visibility",
          "registrableClasses",
          "raceOfficeEnabled",
          "organizer",
          "coOrganizers"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id."
          },
          "name": {
            "type": "string",
            "description": "Event name."
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Venue; null if unknown."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text description written by the organiser (may contain line breaks); null = none."
          },
          "startDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "First day, UTC; null if not set."
          },
          "endDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last day, UTC; null if not set."
          },
          "registrationStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entry opening time, UTC; null if not set."
          },
          "registrationEnd": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entry closing time, UTC; null if not set."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Computed state, as in the list (\u0060Upcoming\u0060, \u0060Live\u0060, \u0060Completed\u0060, \u2026)."
          },
          "coverUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cover image URL, else the poster; null = none."
          },
          "posterUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Poster image URL; null = none."
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "description": "Event website URL; null = none."
          },
          "isOfficial": {
            "type": "boolean",
            "description": "true when the event\u0027s results are declared official."
          },
          "entryCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of entries."
          },
          "countryCount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Number of distinct nations among the entries; null when unknown/none."
          },
          "races": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicRaceItemDto"
            },
            "description": "The races in schedule order (same as GET \u2026/races)."
          },
          "tieBreakStrategy": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tie-break rule used for merged races; null = default."
          },
          "divisions": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicEventDivisionDto"
            },
            "description": "Divisions of the event; null when none."
          },
          "specialCategories": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicSpecialCategoryDto"
            },
            "description": "Special categories of the event; null when none."
          },
          "discardRules": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicDiscardRuleDto"
            },
            "description": "Discard rules; null when none (no discards)."
          },
          "isAwardsPublished": {
            "type": "boolean",
            "description": "true when the organiser has published the prize list."
          },
          "organizerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy public id of the main organising club (same as organizer.publicUid); null for an event without a club."
          },
          "logoUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Event logo URL; null = none."
          },
          "noticeOfRaceUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Notice of Race document URL; null = none."
          },
          "resultsUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "External results URL, if the organiser publishes results elsewhere too; null = none."
          },
          "liveTrackUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Live tracking URL; null = none."
          },
          "instagram": {
            "type": [
              "string",
              "null"
            ],
            "description": "Instagram link/handle; null = none."
          },
          "whatsapp": {
            "type": [
              "string",
              "null"
            ],
            "description": "WhatsApp contact link of the event as published by the organiser; null = none."
          },
          "rrsEventId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Racing Rules of Sailing (World Sailing) event number, if any; null = none."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Event type label (free vocabulary); null = none."
          },
          "classes": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Class names as typed by the organiser (free text, informational); null = none. For entry use registrableClasses."
          },
          "externalRegistrationFormUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Entry form outside Sail Club; null = none."
          },
          "externalPaymentUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Payment page outside Sail Club; null = none."
          },
          "aiAssistantEnabled": {
            "type": "boolean",
            "description": "Whether the Sail Club app offers the event\u0027s AI assistant (informational)."
          },
          "galleryUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Photo gallery URL; null = none."
          },
          "visibility": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always \u0060Published\u0060 here (unpublished events are not visible through the API)."
          },
          "registrableClasses": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicEventClassDto"
            },
            "description": "Classes open for entry, each with its divisions; always an array."
          },
          "raceOfficeEnabled": {
            "type": "boolean",
            "description": "Whether the event runs its race office in Sail Club (informational)."
          },
          "organizer": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PublicEventOrganizerDto"
              },
              {
                "type": "null"
              }
            ],
            "description": "Main organising club; null only for an event without a club."
          },
          "coOrganizers": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicEventOrganizerDto"
            },
            "description": "Co-organising clubs; always an array."
          }
        }
      },
      "PublicRaceItemDto": {
        "type": "object",
        "description": "One race of the event.",
        "required": [
          "id",
          "name",
          "scheduledDate",
          "status",
          "hasResults",
          "resultCount",
          "isOfficial",
          "ratingLabel",
          "coefficient",
          "mergeGroupName",
          "isMergePrimary",
          "raceType",
          "raceNo",
          "replaySource",
          "isMobileBroadcastActive",
          "boatModel"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Race id (UUID) \u2014 use it in /api/v1/races/{raceId}/results."
          },
          "name": {
            "type": "string",
            "description": "Race name, e.g. \u0060Race 3\u0060."
          },
          "scheduledDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Scheduled start, UTC; null if not scheduled."
          },
          "status": {
            "type": "string",
            "description": "Race state, e.g. \u0060Scheduled\u0060, \u0060Started\u0060, \u0060Finished\u0060, \u0060Cancelled\u0060, \u0060Abandoned\u0060."
          },
          "hasResults": {
            "type": "boolean",
            "description": "true when results exist."
          },
          "resultCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of result rows (boats) in the divisions."
          },
          "isOfficial": {
            "type": "boolean",
            "description": "true when the race\u0027s results are declared official."
          },
          "ratingLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rating system the race is scored with (e.g. \u0060ORC ToT\u0060, \u0060IRC\u0060); null = elapsed time."
          },
          "coefficient": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Score multiplier of the race (e.g. 1.5 for a long-distance race); null = 1."
          },
          "mergeGroupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the group of races scored as one leg; null when not merged."
          },
          "isMergePrimary": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Within a merge group: true for the race that carries the leg; null when not merged."
          },
          "raceType": {
            "type": [
              "string",
              "null"
            ],
            "description": "Race kind label (e.g. \u0060Inshore\u0060, \u0060Offshore\u0060); null = none."
          },
          "raceNo": {
            "type": "integer",
            "format": "int32",
            "description": "Sequential race number within the event."
          },
          "replaySource": {
            "type": [
              "string",
              "null"
            ],
            "description": "Replay identifier for the Sail Club race viewer; null = none."
          },
          "isMobileBroadcastActive": {
            "type": "boolean",
            "description": "true while the race is being tracked live in the Sail Club app."
          },
          "boatModel": {
            "type": [
              "string",
              "null"
            ],
            "description": "3D model category for the race viewer; null = default."
          }
        }
      },
      "PublicEventDivisionDto": {
        "type": "object",
        "description": "A division (fleet) boats are scored in, usually a rating band of a class.",
        "required": [
          "id",
          "name",
          "orderNo",
          "colorName",
          "colorHexCode",
          "ratingBegin",
          "ratingEnd",
          "hasOverall",
          "eventOverallGroupId",
          "eventOverallGroupName",
          "eventOverallGroupTitle"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Division id."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Division name."
          },
          "orderNo": {
            "type": "integer",
            "format": "int32",
            "description": "Display order."
          },
          "colorName": {
            "type": "string",
            "description": "Display colour name of the division (e.g. \u0060Blue\u0060), for badges."
          },
          "colorHexCode": {
            "type": "string",
            "description": "Display colour of the division, \u0060#rrggbb\u0060."
          },
          "ratingBegin": {
            "type": "number",
            "description": "Lower bound of the rating band (inclusive)."
          },
          "ratingEnd": {
            "type": "number",
            "description": "Upper bound of the rating band."
          },
          "hasOverall": {
            "type": "boolean",
            "description": "true when the division is part of an overall ranking."
          },
          "eventOverallGroupId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group id; null when none."
          },
          "eventOverallGroupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group name; null when none."
          },
          "eventOverallGroupTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group display title; null when none."
          }
        }
      },
      "PublicEventClassDto": {
        "type": "object",
        "description": "A boat class open for entry, with its divisions.",
        "required": [
          "id",
          "name",
          "orderNo",
          "divisions"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Class id."
          },
          "name": {
            "type": "string",
            "description": "Class name."
          },
          "orderNo": {
            "type": "integer",
            "format": "int32",
            "description": "Display order."
          },
          "divisions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicEventDivisionDto"
            },
            "description": "Divisions of this class; always an array."
          }
        }
      },
      "PublicSpecialCategoryDto": {
        "type": "object",
        "description": "A special category (e.g. youth, female helm) with its own ranking.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Special category id."
          },
          "name": {
            "type": "string",
            "description": "Category name."
          }
        }
      },
      "PublicDiscardRuleDto": {
        "type": "object",
        "description": "Discard rule: from \u0060minCompletedRaces\u0060 completed races on, each boat\u0027s \u0060discardCount\u0060 worst scores are dropped.",
        "required": [
          "id",
          "minCompletedRaces",
          "discardCount",
          "targetCoefficient"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Rule id."
          },
          "minCompletedRaces": {
            "type": "integer",
            "format": "int32",
            "description": "Completed races needed for the rule to apply."
          },
          "discardCount": {
            "type": "integer",
            "format": "int32",
            "description": "Number of worst scores dropped."
          },
          "targetCoefficient": {
            "type": [
              "number",
              "null"
            ],
            "description": "Applies only to races with this coefficient; null = all races."
          }
        }
      },
      "PublicEventOrganizerDto": {
        "type": "object",
        "description": "An organising club.",
        "required": [
          "id",
          "publicUid",
          "name",
          "shortName",
          "logoUrl",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Canonical club id."
          },
          "publicUid": {
            "type": "string",
            "description": "Club id used in public club page URLs."
          },
          "name": {
            "type": "string",
            "description": "Club name."
          },
          "shortName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short name; null = none."
          },
          "logoUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Club logo URL; null = none."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Club type label; null = none."
          }
        }
      },
      "PublicRaceResultPageDto": {
        "type": "object",
        "description": "One race\u0027s results page.",
        "required": [
          "id",
          "name",
          "eventId",
          "eventName",
          "scheduledDate",
          "isOfficial",
          "results",
          "courseDistanceNm",
          "scoringModels",
          "resultsUpdatedAtUtc",
          "courses",
          "startGroupSummaries"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Race id."
          },
          "name": {
            "type": "string",
            "description": "Race name."
          },
          "eventId": {
            "type": "string",
            "description": "Event id."
          },
          "eventName": {
            "type": "string",
            "description": "Event name."
          },
          "scheduledDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Scheduled start, UTC; null if not scheduled."
          },
          "isOfficial": {
            "type": "boolean",
            "description": "true when the results are official."
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RaceResultDto"
            },
            "description": "One row per boat, ordered by division then rank. Empty before results exist or when the race is cancelled."
          },
          "courseDistanceNm": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Sailed distance in nautical miles when all start groups sailed the same; null otherwise."
          },
          "scoringModels": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "string"
            },
            "description": "Scoring model per rating system as chosen by the race officer, e.g. {\u0022ORC\u0022:\u0022PCS_WL\u0022}; null = default."
          },
          "resultsUpdatedAtUtc": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the results last changed, UTC; null before results exist."
          },
          "courses": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicCourseDto"
            },
            "description": "Applied course per start group (with map geometry); may be empty."
          },
          "startGroupSummaries": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicStartGroupSummaryDto"
            },
            "description": "Start groups in start order; always present."
          }
        }
      },
      "RaceResultDto": {
        "type": "object",
        "description": "One boat\u0027s result in a race.",
        "required": [
          "id",
          "raceId",
          "entryId",
          "startGroupId",
          "sailNumber",
          "boatName",
          "teamName",
          "skipper",
          "owner",
          "divisionId",
          "divisionName",
          "divisionColorName",
          "divisionColorHexCode",
          "className",
          "modelClass",
          "status",
          "finishTimeUtc",
          "elapsedTime",
          "correctedTime",
          "points",
          "rank",
          "eventOverallGroupId",
          "overallGroupName",
          "overallGroupTitle",
          "listOrderNo",
          "raiting",
          "raitingLabel",
          "startTimeUtc",
          "penaltyPoints",
          "timePenaltySeconds",
          "penalties"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Result row id."
          },
          "raceId": {
            "type": "string",
            "format": "uuid",
            "description": "Race id."
          },
          "entryId": {
            "type": "string",
            "format": "uuid",
            "description": "Entry (boat registration) id \u2014 stable across the event\u0027s races."
          },
          "startGroupId": {
            "type": "string",
            "format": "uuid",
            "description": "Start group id."
          },
          "sailNumber": {
            "type": "string",
            "description": "Sail number as entered on the entry."
          },
          "boatName": {
            "type": "string",
            "description": "Boat name as entered on the entry."
          },
          "teamName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Team name; null/empty = none."
          },
          "skipper": {
            "type": [
              "string",
              "null"
            ],
            "description": "Skipper/helm name as entered on the entry; null if not given."
          },
          "owner": {
            "type": [
              "string",
              "null"
            ],
            "description": "Owner name as entered on the entry; null if not given."
          },
          "divisionId": {
            "type": "string",
            "format": "uuid",
            "description": "Division id."
          },
          "divisionName": {
            "type": "string",
            "description": "Division name."
          },
          "divisionColorName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour name of the division (e.g. \u0060Blue\u0060), for badges."
          },
          "divisionColorHexCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour of the division, \u0060#rrggbb\u0060."
          },
          "className": {
            "type": [
              "string",
              "null"
            ],
            "description": "Boat class name."
          },
          "modelClass": {
            "type": [
              "string",
              "null"
            ],
            "description": "Boat model (design), e.g. \u0060First 40.7\u0060; null = unknown."
          },
          "status": {
            "type": "string",
            "description": "Finish status: \u0060Finished\u0060, or a scoring code such as DNF, DNS, DNC, OCS, RET, DSQ, UFD, BFD, SCP, ZFP, DPI, RDG, NSC, DNE, DGM."
          },
          "finishTimeUtc": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Finish time, UTC; null when not finished."
          },
          "elapsedTime": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?(\\d\u002B\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
            "description": "Elapsed time (start to finish), \u0060[d.]hh:mm:ss[.fffffff]\u0060; null when not finished."
          },
          "correctedTime": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?(\\d\u002B\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
            "description": "Handicap-corrected time, same format; null when not applicable."
          },
          "points": {
            "type": "number",
            "description": "Points scored in this race (low point: fewer is better)."
          },
          "rank": {
            "type": "integer",
            "format": "int32",
            "description": "Place in the division."
          },
          "eventOverallGroupId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group id when this row belongs to an overall ranking; null for division rows."
          },
          "overallGroupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group name; null for division rows."
          },
          "overallGroupTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group title; null for division rows."
          },
          "listOrderNo": {
            "type": "integer",
            "format": "int32",
            "description": "Display order within the list."
          },
          "raiting": {
            "type": "number",
            "description": "Rating value used for the corrected time (0 = none)."
          },
          "raitingLabel": {
            "type": "string",
            "description": "Name of the rating used (e.g. \u0060ORC ToT Inshore\u0060)."
          },
          "startTimeUtc": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Start time of the boat\u0027s start group, UTC; null if unknown."
          },
          "penaltyPoints": {
            "type": [
              "number",
              "null"
            ],
            "description": "Points a places penalty added (already included in the score); null = none."
          },
          "timePenaltySeconds": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Seconds a time penalty added (already included in the corrected time); null = none."
          },
          "penalties": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FinishPenaltyInfoDto"
            },
            "description": "The penalties applied (for a tooltip); null = none."
          }
        }
      },
      "FinishPenaltyInfoDto": {
        "type": "object",
        "description": "A penalty applied to this result by the race committee.",
        "required": [
          "type",
          "value",
          "description"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Penalty kind, e.g. \u0060TimePercent\u0060, \u0060TimeSeconds\u0060, \u0060Places\u0060."
          },
          "value": {
            "type": "number",
            "description": "Penalty size in the kind\u0027s unit."
          },
          "description": {
            "type": "string",
            "description": "Committee note as typed (may be empty)."
          }
        }
      },
      "PublicCourseDto": {
        "type": "object",
        "description": "The course a start group sailed (geometry for a map).",
        "required": [
          "id",
          "startGroupId",
          "startGroupName",
          "courseName",
          "courseType",
          "twd",
          "uphaulLegNm",
          "startLineLengthM",
          "finishLineLengthM",
          "startType",
          "startSide",
          "finishSide",
          "leewardGateWidthM",
          "leewardGateOffsetM",
          "offsetDistanceM",
          "hasOffsetMark",
          "hasLeewardGate",
          "roundingDirection",
          "totalDistanceNm",
          "legs",
          "isApplied",
          "createdAt",
          "markPositions",
          "routeSequence",
          "marks",
          "avgLoaM",
          "smartAssistant"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Course id."
          },
          "startGroupId": {
            "type": "string",
            "format": "uuid",
            "description": "Start group that sailed it."
          },
          "startGroupName": {
            "type": "string",
            "description": "Start group name."
          },
          "courseName": {
            "type": "string",
            "description": "Course name."
          },
          "courseType": {
            "type": "string",
            "description": "Course shape, e.g. \u0060WindwardLeeward\u0060, \u0060Triangle\u0060, \u0060Custom\u0060."
          },
          "twd": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "True wind direction in degrees; null if unknown."
          },
          "uphaulLegNm": {
            "type": "number",
            "format": "double",
            "description": "Windward leg length, nautical miles."
          },
          "startLineLengthM": {
            "type": "number",
            "format": "double",
            "description": "Start line length, metres."
          },
          "finishLineLengthM": {
            "type": "number",
            "format": "double",
            "description": "Finish line length, metres."
          },
          "startType": {
            "type": "string",
            "description": "Start line position, e.g. \u0060Windward\u0060/\u0060Leeward\u0060."
          },
          "startSide": {
            "type": "string",
            "description": "Start side, \u0060Starboard\u0060/\u0060Port\u0060."
          },
          "finishSide": {
            "type": "string",
            "description": "Finish side, \u0060Starboard\u0060/\u0060Port\u0060."
          },
          "leewardGateWidthM": {
            "type": "number",
            "format": "double",
            "description": "Leeward gate width, metres."
          },
          "leewardGateOffsetM": {
            "type": "number",
            "format": "double",
            "description": "Leeward gate offset, metres."
          },
          "offsetDistanceM": {
            "type": "number",
            "format": "double",
            "description": "Offset mark distance, metres."
          },
          "hasOffsetMark": {
            "type": "boolean",
            "description": "true when the course has an offset mark."
          },
          "hasLeewardGate": {
            "type": "boolean",
            "description": "true when the course has a leeward gate."
          },
          "roundingDirection": {
            "type": [
              "string",
              "null"
            ],
            "description": "Default rounding direction \u0060CW\u0060/\u0060CCW\u0060; per-mark values in marks; null = default."
          },
          "totalDistanceNm": {
            "type": "number",
            "format": "double",
            "description": "Total course length, nautical miles."
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CourseLegDto"
            },
            "description": "Legs in order."
          },
          "isApplied": {
            "type": "boolean",
            "description": "Always true here (only applied courses are returned)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the course was created, UTC."
          },
          "markPositions": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/CoordinateDto"
            },
            "description": "Mark key \u21D2 position (WGS84)."
          },
          "routeSequence": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RouteSequenceItemDto"
            },
            "description": "Rounding order."
          },
          "marks": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/MarkRoundingConfigDto"
            },
            "description": "Per-mark rounding side; null = default for all."
          },
          "avgLoaM": {
            "type": "number",
            "format": "double",
            "description": "Average boat length used for line sizing, metres."
          },
          "smartAssistant": {
            "type": "boolean",
            "description": "Whether the course was laid with the assistant (informational)."
          }
        }
      },
      "CourseLegDto": {
        "type": "object",
        "description": "One leg of a course.",
        "required": [
          "label",
          "bearingTrue",
          "distanceNm"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "Leg label."
          },
          "bearingTrue": {
            "type": "integer",
            "format": "int32",
            "description": "True bearing, degrees."
          },
          "distanceNm": {
            "type": "number",
            "format": "double",
            "description": "Length, nautical miles."
          }
        }
      },
      "CoordinateDto": {
        "type": "object",
        "description": "A WGS84 position.",
        "required": [
          "lat",
          "lng"
        ],
        "properties": {
          "lat": {
            "type": "number",
            "format": "double",
            "description": "Latitude, decimal degrees."
          },
          "lng": {
            "type": "number",
            "format": "double",
            "description": "Longitude, decimal degrees."
          }
        }
      },
      "RouteSequenceItemDto": {
        "type": "object",
        "description": "One step of the rounding order.",
        "required": [
          "uid",
          "markKey",
          "label"
        ],
        "properties": {
          "uid": {
            "type": "string",
            "description": "Step id."
          },
          "markKey": {
            "type": "string",
            "description": "Mark key (see markPositions)."
          },
          "label": {
            "type": "string",
            "description": "Step label."
          }
        }
      },
      "MarkRoundingConfigDto": {
        "type": "object",
        "description": "Which side a mark is rounded.",
        "required": [
          "key",
          "rounding"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Mark key."
          },
          "rounding": {
            "type": "string",
            "description": "\u0060CW\u0060 (to starboard) or \u0060CCW\u0060 (to port)."
          }
        }
      },
      "PublicStartGroupSummaryDto": {
        "type": "object",
        "description": "A start group (boats starting together) of the race.",
        "required": [
          "startGroupId",
          "startGroupName",
          "registeredBoatCount",
          "divisionNames",
          "plannedStartTimeUtc",
          "courseDistanceNm"
        ],
        "properties": {
          "startGroupId": {
            "type": "string",
            "format": "uuid",
            "description": "Start group id."
          },
          "startGroupName": {
            "type": "string",
            "description": "Start group name."
          },
          "registeredBoatCount": {
            "type": "integer",
            "format": "int32",
            "description": "Approved entries in the group\u0027s divisions."
          },
          "divisionNames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Divisions starting in this group."
          },
          "plannedStartTimeUtc": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Planned start, UTC; null if not set."
          },
          "courseDistanceNm": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Distance override for this group, nautical miles; null = the course length."
          }
        }
      },
      "PublicApiEventResultsDto": {
        "type": "object",
        "description": "The event\u0027s four public result lists.",
        "required": [
          "results",
          "overall",
          "ratingGroups",
          "specialCategories"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventResultDto"
            },
            "description": "Series results per division, ordered by division then rank."
          },
          "overall": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventResultDto"
            },
            "description": "Overall rankings across divisions (one per overall group); may be empty."
          },
          "ratingGroups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventRatingGroupResultDto"
            },
            "description": "Separate rankings per rating family (\u0060ratingGroup\u0060 = \u0060IRC\u0060 or \u0060ORC\u0060); may be empty."
          },
          "specialCategories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicSpecialCategoryResultDto"
            },
            "description": "Special-category rankings; may be empty."
          }
        }
      },
      "EventResultDto": {
        "type": "object",
        "description": "One boat\u0027s series (event) result.",
        "required": [
          "id",
          "eventId",
          "entryId",
          "divisionId",
          "eventOverallGroupId",
          "overallGroupName",
          "overallGroupTitle",
          "points",
          "score",
          "rank",
          "sailNumber",
          "boatName",
          "teamName",
          "divisionName",
          "divisionColorName",
          "divisionColorHexCode",
          "createdOn",
          "updatedOn",
          "divisionOrderNo",
          "boatNo",
          "skipper",
          "owner",
          "status",
          "className",
          "modelClass",
          "isSeriesValid"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Result row id."
          },
          "eventId": {
            "type": "string",
            "description": "Event id."
          },
          "entryId": {
            "type": "string",
            "format": "uuid",
            "description": "Entry (boat registration) id."
          },
          "divisionId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Division id; null for overall rows."
          },
          "eventOverallGroupId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group id; null for division rows."
          },
          "overallGroupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group name; null for division rows."
          },
          "overallGroupTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group title; null for division rows."
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreItemDto"
            },
            "description": "Per-race scores in race order (discarded ones flagged)."
          },
          "score": {
            "type": "number",
            "description": "Net series score (after discards; low point: fewer is better)."
          },
          "rank": {
            "type": "integer",
            "format": "int32",
            "description": "Series place."
          },
          "sailNumber": {
            "type": "string",
            "description": "Sail number as entered on the entry."
          },
          "boatName": {
            "type": "string",
            "description": "Boat name as entered on the entry."
          },
          "teamName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Team name; null = none."
          },
          "divisionName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Division name; null for overall rows."
          },
          "divisionColorName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour name of the division (e.g. \u0060Blue\u0060), for badges."
          },
          "divisionColorHexCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour of the division, \u0060#rrggbb\u0060."
          },
          "createdOn": {
            "type": "string",
            "format": "date-time",
            "description": "When the row was first calculated, UTC."
          },
          "updatedOn": {
            "type": "string",
            "format": "date-time",
            "description": "When the row was last recalculated, UTC."
          },
          "divisionOrderNo": {
            "type": "integer",
            "format": "int32",
            "description": "Division display order."
          },
          "boatNo": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Bow number; null = none."
          },
          "skipper": {
            "type": [
              "string",
              "null"
            ],
            "description": "Skipper/helm name as entered on the entry; null if not given."
          },
          "owner": {
            "type": [
              "string",
              "null"
            ],
            "description": "Owner name as entered on the entry; null if not given."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Series status code if any; null = normal."
          },
          "className": {
            "type": "string",
            "description": "Boat class name."
          },
          "modelClass": {
            "type": [
              "string",
              "null"
            ],
            "description": "Boat model (design); null = unknown."
          },
          "isSeriesValid": {
            "type": "boolean",
            "description": "false when fewer races were sailed than the series minimum (ranking shown but not valid)."
          }
        }
      },
      "ScoreItemDto": {
        "type": "object",
        "description": "The boat\u0027s score in one race of the series.",
        "required": [
          "raceNo",
          "legNo",
          "raceName",
          "score",
          "isDiscarded",
          "penalty",
          "isPrimary",
          "elapsedTime",
          "correctedTime",
          "didBreakTie",
          "mergedTotalScore",
          "penaltyPoints",
          "timePenaltySeconds",
          "penalties"
        ],
        "properties": {
          "raceNo": {
            "type": "integer",
            "format": "int32",
            "description": "Race number."
          },
          "legNo": {
            "type": "integer",
            "format": "int32",
            "description": "Leg number (merged races share one)."
          },
          "raceName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Race name; null if unknown."
          },
          "score": {
            "type": "number",
            "description": "Score in this race (not the merged total)."
          },
          "isDiscarded": {
            "type": "boolean",
            "description": "true when this score is dropped by a discard rule."
          },
          "penalty": {
            "type": [
              "string",
              "null"
            ],
            "description": "Scoring code (DNF, DNS, OCS, \u2026); null when finished normally."
          },
          "isPrimary": {
            "type": "boolean",
            "description": "true for the race that carries a merged leg."
          },
          "elapsedTime": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?(\\d\u002B\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
            "description": "Elapsed time, \u0060[d.]hh:mm:ss[.fffffff]\u0060; null if none."
          },
          "correctedTime": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?(\\d\u002B\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
            "description": "Corrected time, same format; null if none."
          },
          "didBreakTie": {
            "type": "boolean",
            "description": "true when this race decided a tie."
          },
          "mergedTotalScore": {
            "type": [
              "number",
              "null"
            ],
            "description": "Total of the merged leg; null for single-race legs."
          },
          "penaltyPoints": {
            "type": [
              "number",
              "null"
            ],
            "description": "Points a places penalty added (already included in the score); null = none."
          },
          "timePenaltySeconds": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Seconds a time penalty added (already included in the corrected time); null = none."
          },
          "penalties": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FinishPenaltyInfoDto"
            },
            "description": "The penalties applied (for a tooltip); null = none."
          }
        }
      },
      "EventRatingGroupResultDto": {
        "type": "object",
        "description": "One boat\u0027s result in a rating-family ranking (IRC-only or ORC-only).",
        "required": [
          "ratingGroup",
          "id",
          "eventId",
          "entryId",
          "divisionId",
          "eventOverallGroupId",
          "overallGroupName",
          "overallGroupTitle",
          "points",
          "score",
          "rank",
          "sailNumber",
          "boatName",
          "teamName",
          "divisionName",
          "divisionColorName",
          "divisionColorHexCode",
          "createdOn",
          "updatedOn",
          "divisionOrderNo",
          "boatNo",
          "skipper",
          "owner",
          "status",
          "className",
          "modelClass",
          "isSeriesValid"
        ],
        "properties": {
          "ratingGroup": {
            "type": "string",
            "description": "Rating family of this ranking: \u0060IRC\u0060 or \u0060ORC\u0060."
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Result row id."
          },
          "eventId": {
            "type": "string",
            "description": "Event id."
          },
          "entryId": {
            "type": "string",
            "format": "uuid",
            "description": "Entry (boat registration) id."
          },
          "divisionId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Division id; null for overall rows."
          },
          "eventOverallGroupId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group id; null for division rows."
          },
          "overallGroupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group name; null for division rows."
          },
          "overallGroupTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Overall group title; null for division rows."
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreItemDto"
            },
            "description": "Per-race scores in race order (discarded ones flagged)."
          },
          "score": {
            "type": "number",
            "description": "Net series score (after discards; low point: fewer is better)."
          },
          "rank": {
            "type": "integer",
            "format": "int32",
            "description": "Series place."
          },
          "sailNumber": {
            "type": "string",
            "description": "Sail number as entered on the entry."
          },
          "boatName": {
            "type": "string",
            "description": "Boat name as entered on the entry."
          },
          "teamName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Team name; null = none."
          },
          "divisionName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Division name; null for overall rows."
          },
          "divisionColorName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour name of the division (e.g. \u0060Blue\u0060), for badges."
          },
          "divisionColorHexCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display colour of the division, \u0060#rrggbb\u0060."
          },
          "createdOn": {
            "type": "string",
            "format": "date-time",
            "description": "When the row was first calculated, UTC."
          },
          "updatedOn": {
            "type": "string",
            "format": "date-time",
            "description": "When the row was last recalculated, UTC."
          },
          "divisionOrderNo": {
            "type": "integer",
            "format": "int32",
            "description": "Division display order."
          },
          "boatNo": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Bow number; null = none."
          },
          "skipper": {
            "type": [
              "string",
              "null"
            ],
            "description": "Skipper/helm name as entered on the entry; null if not given."
          },
          "owner": {
            "type": [
              "string",
              "null"
            ],
            "description": "Owner name as entered on the entry; null if not given."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Series status code if any; null = normal."
          },
          "className": {
            "type": "string",
            "description": "Boat class name."
          },
          "modelClass": {
            "type": [
              "string",
              "null"
            ],
            "description": "Boat model (design); null = unknown."
          },
          "isSeriesValid": {
            "type": "boolean",
            "description": "false when fewer races were sailed than the series minimum (ranking shown but not valid)."
          }
        }
      },
      "PublicSpecialCategoryResultDto": {
        "type": "object",
        "description": "One boat\u0027s result in a special category.",
        "required": [
          "id",
          "specialCategoryId",
          "categoryName",
          "sailNumber",
          "boatName",
          "totalScore",
          "rank",
          "points",
          "skipper"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Result row id."
          },
          "specialCategoryId": {
            "type": "string",
            "format": "uuid",
            "description": "Special category id."
          },
          "categoryName": {
            "type": "string",
            "description": "Category name."
          },
          "sailNumber": {
            "type": "string",
            "description": "Sail number as entered on the entry."
          },
          "boatName": {
            "type": "string",
            "description": "Boat name as entered on the entry."
          },
          "totalScore": {
            "type": "number",
            "description": "Net score within the category."
          },
          "rank": {
            "type": "integer",
            "format": "int32",
            "description": "Place within the category."
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicSpecialCategoryRacePointDto"
            },
            "description": "Per-race scores within the category."
          },
          "skipper": {
            "type": [
              "string",
              "null"
            ],
            "description": "Skipper/helm name as entered on the entry; null if not given."
          }
        }
      },
      "PublicSpecialCategoryRacePointDto": {
        "type": "object",
        "description": "The boat\u0027s score in one race, within the special category.",
        "required": [
          "raceNo",
          "score",
          "isDiscarded",
          "penalty",
          "penaltyPoints",
          "timePenaltySeconds",
          "penalties"
        ],
        "properties": {
          "raceNo": {
            "type": "integer",
            "format": "int32",
            "description": "Race number."
          },
          "score": {
            "type": "number",
            "description": "Score within the category."
          },
          "isDiscarded": {
            "type": "boolean",
            "description": "true when dropped by a discard rule."
          },
          "penalty": {
            "type": [
              "string",
              "null"
            ],
            "description": "Scoring code; null when finished normally."
          },
          "penaltyPoints": {
            "type": [
              "number",
              "null"
            ],
            "description": "Points a places penalty added (already included in the score); null = none."
          },
          "timePenaltySeconds": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Seconds a time penalty added (already included in the corrected time); null = none."
          },
          "penalties": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FinishPenaltyInfoDto"
            },
            "description": "The penalties applied (for a tooltip); null = none."
          }
        }
      },
      "TokenInfo": {
        "type": "object",
        "description": "What a token is.",
        "required": [
          "client_id",
          "club_id",
          "scope",
          "expires_at"
        ],
        "properties": {
          "client_id": {
            "type": "string",
            "description": "The client id (\u0060scc_\u2026\u0060)."
          },
          "club_id": {
            "type": "string",
            "description": "Canonical id of the club whose data the token reads."
          },
          "scope": {
            "type": "string",
            "description": "Space-separated scopes."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Expiry, UTC."
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "description": "RFC 6749 \u00A75.1 token response.",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Opaque Bearer token (\u0060sca_\u2026\u0060, 43\u002B chars). Send as \u0060Authorization: Bearer \u003Ctoken\u003E\u0060. Do not parse or log it."
          },
          "token_type": {
            "type": "string",
            "description": "Always \u0060Bearer\u0060."
          },
          "expires_in": {
            "type": "integer",
            "description": "Lifetime in seconds (3600)."
          },
          "scope": {
            "type": "string",
            "description": "Space-separated scopes the token carries."
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "description": "Token endpoint error (RFC 6749 \u00A75.2) plus our machine code. No error_description.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "RFC 6749 error: invalid_request \u00B7 invalid_client \u00B7 unauthorized_client \u00B7 unsupported_grant_type \u00B7 invalid_scope \u00B7 rate_limited."
          },
          "code": {
            "type": "string",
            "description": "Stable machine code, e.g. API_CLIENT_CREDENTIALS_INVALID."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. Switch on \u0060code\u0060; there is no human sentence (title/detail) by design.",
        "required": [
          "type",
          "status",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "\u0060https://sailracing.club/docs/errors#\u003Ccode\u003E\u0060 \u2014 documentation link for the code."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status, repeated."
          },
          "code": {
            "type": "string",
            "description": "Stable machine code (see the enum).",
            "enum": [
              "API_TOKEN_REQUEST_INVALID",
              "API_GRANT_TYPE_UNSUPPORTED",
              "API_CLIENT_CREDENTIALS_INVALID",
              "API_CLIENT_DISABLED",
              "API_CLIENT_EXPIRED",
              "API_IP_NOT_ALLOWED",
              "API_SCOPE_INVALID",
              "API_TOKEN_MISSING",
              "API_TOKEN_INVALID",
              "API_SCOPE_MISSING",
              "API_PARAM_INVALID",
              "EVENT_NOT_FOUND",
              "RACE_NOT_FOUND",
              "RATE_LIMIT_EXCEEDED",
              "API_UNAVAILABLE",
              "INTERNAL_ERROR"
            ]
          },
          "traceId": {
            "type": "string",
            "description": "Request id \u2014 quote it when you contact support."
          },
          "params": {
            "type": "object",
            "description": "Only for codes with parameters: API_SCOPE_MISSING / API_SCOPE_INVALID \u21D2 \u0060scope\u0060; API_PARAM_INVALID \u21D2 \u0060name\u0060.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}