Skip to content
Developers
OpenAPI

Overview

API v1 Last updated 2026-09-30 View as Markdown

#Sail Club API

The Sail Club API lets a club's own website (or any server-side integration the club authorises) read the club's published regatta data — events, races, race results and event results — the same data the club's public results pages already show, as clean JSON instead of scraped HTML.

#Base URL

https://sail-club-server.cloud.run

Every path on this site (/api/v1/…) is relative to that host.

#The five-minute path

  1. Ask your club's admin for Settings → API access credentials (a client_id and a client_secret, shown once). See Quickstart.
  2. Exchange them for a bearer token: POST /api/v1/oauth/token.
  3. Call the data endpoints with Authorization: Bearer <token>: events · event detail · races · race results · event results.
  4. Cache the response yourself for at least the Cache-Control: max-age you receive back, and use If-None-Match with the ETag on your next call — see Caching & freshness.

#Scopes

Two read scopes exist today: events:read (events, event detail, races) and results:read (race results, event results). Full table: Scopes.

#Errors

Every non-2xx response on /api/v1 (except the token endpoint, which follows RFC 6749) is application/problem+json with a machine-readable code. Full table with fixes: Errors.

#Data models

Every response is exactly the same DTO shape the public results pages already render — nothing new to learn if you've looked at a club's public event page. Full field tables: Data models.

#Build with AI

Every method page on this site carries a ready-made Prompt block for handing to an AI coding agent — see Build with AI for the idea, or use the full-integration prompt below to have an agent build the whole client in one pass.

#Full-integration prompt

This prompt is deliberately self-contained — a coding agent that reads only this block (no other page on this site) can build a working client. It builds a small results-page integration: the club's upcoming events, and for a finished event, its overall standings — the same shape of page a club typically wants on its own website.

Prompt for AI agents ready to paste into an agent
You are building a server-side client for the Sail Club API — a read-only, server-to-server REST API for
club regatta data. Do not put credentials or tokens in any client-side/browser code; this API has no CORS
and must only ever be called from a backend.

BASE URL
  https://sail-club-server.cloud.run

AUTHENTICATION (OAuth 2.0 client credentials, RFC 6749 §4.4)
  You have a client_id (starts "scc_") and a client_secret (starts "scs_") from the club admin. Read both
  from environment variables — never hard-code or log them. Both only ever use the base64url alphabet
  (letters, digits, "-", "_") plus their fixed prefix, so send them as-is — no percent-encoding step needed.
  Request:
    POST /api/v1/oauth/token
    Content-Type: application/x-www-form-urlencoded
    Authorization: Basic base64(client_id + ":" + client_secret)
    Body: grant_type=client_credentials
  Response 200 (Cache-Control: no-store):
    { "access_token": "sca_...", "token_type": "Bearer", "expires_in": 3600, "scope": "events:read results:read" }
  The token is opaque — do not decode it. It is valid for expires_in seconds (1 hour). Cache it in memory
  (per process) and reuse it; fetch a new one only when a call returns 401, or 60 seconds before expires_in
  runs out. Never request a new token per API call — the token endpoint is rate-limited.
  Errors: RFC 6749 JSON body { "error": "...", ... } plus our own "code" field. A 429 "rate_limited" means
  back off for Retry-After seconds (see RATE LIMIT HANDLING below) and retry; every other non-200 is a hard
  failure for this run — surface the "code" field to logs and stop.

DATA CALLS
  Authorization: Bearer <access_token> on every call below. No club id parameter — the token determines the
  club. All 4xx/5xx bodies are application/problem+json: { "type", "status", "code", "params"?, "traceId"? }.

  1. GET /api/v1/events?page=1&pageSize=20
     -> { "items": [ { "id", "name", "location", "startDate", "endDate", "status", "coverUrl", "posterUrl",
          "entryCount" } ], "nextPage": <int|null>, "totalItems": <int> }
     Newest startDate first. 404 is never returned here; an empty/no-op club returns an empty items array.

  2. GET /api/v1/events/{eventId}
     -> full event detail: name, dates, location, description, links, races[] (id, name, scheduledDate,
        status, hasResults, resultCount, ...). 404 { "code": "EVENT_NOT_FOUND" } if the id is unknown, not
        this club's, or not published.

  3. GET /api/v1/events/{eventId}/races
     -> [ { "id", "name", "scheduledDate", "status", "hasResults", "isOfficial", "resultCount", "raceNo",
          "raceType", "boatModel" } ] — the same list as the event detail's races[], as its own endpoint.
        404 { "code": "EVENT_NOT_FOUND" } under the same rule as above. Use an item's "id" as raceId below
        once its "hasResults" is true.

  4. GET /api/v1/races/{raceId}/results
     -> { "id", "name", "eventId", "eventName", "scheduledDate", "results": [ { "rank", "sailNumber",
          "boatName", "skipper", "owner", "status" ("Finished"|"DNF"|"DNS"|"OCS"|"RET"|"DSQ"), "elapsedTime",
          "correctedTime", "points", ... } ] }. 404 { "code": "RACE_NOT_FOUND" } if the id is unknown,
        malformed, or another club's.

  5. GET /api/v1/events/{eventId}/results
     -> { "results": [...], "overall": [...], "ratingGroups": [...], "specialCategories": [...] } — each
        entry has rank, sailNumber, boatName, skipper/owner, score/points (specialCategories: totalScore),
        per-race points with isDiscarded. 404 { "code": "EVENT_NOT_FOUND" } under the same rule as above.

  The shapes above are enough to build a working client — nothing else is required. If your stack has an
  OpenAPI code generator, GET /api/v1/openapi.json (OpenAPI 3.1, no auth needed) is an optional,
  always-current source to generate exact types from instead of hand-typing the shapes above.

CACHING
  Every 200/304 carries Cache-Control (private, max-age=<seconds>), ETag, Last-Modified, X-Data-As-Of. Store
  the ETag per URL; send it back as If-None-Match on your next call to that same URL. A 304 has no body —
  keep using your cached copy. Never poll more often than the max-age you were given; data changes at most
  once an hour at our end.

ERROR + RATE LIMIT HANDLING
  401 API_TOKEN_MISSING / API_TOKEN_INVALID -> fetch a fresh token and retry the call once; if it still
    fails, stop and alert a human (the credential itself is the problem).
  401 API_CLIENT_DISABLED / API_CLIENT_EXPIRED -> do NOT refresh: the token endpoint answers 400
    unauthorized_client for a disabled/expired client, so a refresh cannot help. Stop immediately and alert
    a human (needs a club-admin fix in the panel).
  403 API_SCOPE_MISSING / API_IP_NOT_ALLOWED -> not fixable by retrying; alert a human (needs a club-admin
    change in the panel).
  404 EVENT_NOT_FOUND / RACE_NOT_FOUND -> the id doesn't exist for this club (or isn't published) — do not
    retry, show a normal "not found".
  429 RATE_LIMIT_EXCEEDED (Retry-After: 60) / 503 API_UNAVAILABLE (Retry-After: 30) -> Retry-After is always
    an integer number of seconds, never an HTTP date. Back off for exactly that long before retrying, with
    jitter if this runs on a schedule for many events at once.
  Anything else (5xx) -> log the traceId field and retry up to 3 attempts total, with exponential backoff
    (1s, then 2s, then 4s).

WHAT TO BUILD
  1. A small typed API client module covering all five calls above: getToken() (caches + refreshes),
     listEvents({page,pageSize}), getEvent(eventId), listRaces(eventId), getRaceResults(raceId),
     getEventResults(eventId) — each handling the caching/ETag and error/retry rules above.
  2. A minimal test (mocked HTTP) proving: a 401 triggers exactly one token refresh and retry; a 304 reuses
     the cached body; a 429 waits for Retry-After before retrying.
  3. A tiny example that prints the next 5 upcoming events, and for the most recent finished one, its overall
     top 10 (rank, sailNumber, boatName, score) plus its most recent race's own top 3 finishers.

Adapt the above to whatever stack you are actually working in (Node/TypeScript, Python, C#/.NET, PHP, Go,
etc.) — the flow, headers, error codes and caching rules are the same regardless of language.