# Navy Golf API guide

The Birdie Game at Naval Academy Golf Course exposes JSON REST endpoints at `https://navygolf.app/api/v1`. The public website and API use the same records and validation. Read the [OpenAPI 3.1 specification](/api/openapi.json) for exact schemas; the [API catalog](/.well-known/api-catalog) and [Agent Skills index](/.well-known/agent-skills/index.json) provide machine-readable discovery.

## Start with a public read

```sh
curl --fail --silent --show-error https://navygolf.app/api/v1/seasons
curl --fail --silent --show-error https://navygolf.app/api/v1/leaderboard
curl --fail --silent --show-error https://navygolf.app/api/v1/rules
```

Public reads require no account. Select a season from `/seasons` and pass its returned `id` as the URL-encoded `seasonId` query parameter to consistently read that season. Endpoints accepting an optional season select the latest when it is omitted. An unknown season or player returns `404`; an installation with no season may also return `404` for season-dependent reads. `/seasons` and `/rules` can be read without an existing season.

Use returned IDs rather than guessing names or slugs. Check `isSample` and `settingsStatus` before describing a season as confirmed. Null settings are unknown, and draft rules include decisions still to be finalized.

| Job | Public request |
| --- | --- |
| List seasons | `GET /api/v1/seasons` |
| Read selected season settings | `GET /api/v1/season?seasonId=...` |
| Read standings, team holes, and roster contributions | `GET /api/v1/leaderboard?seasonId=...` |
| Find teams or rostered players by name | `GET /api/v1/teams?seasonId=...&search=...` |
| Read one public player or contribution statistics | `GET /api/v1/players/{playerid}?seasonId=...` or `GET /api/v1/players/{playerid}/stats?seasonId=...` |
| Read credited birdies | `GET /api/v1/feed?seasonId=...&limit=20` |
| Read the fixed 18-hole scorecard and completion counts | `GET /api/v1/course?seasonId=...&tee=White` (also `Gold`) |
| Read a hole's credited birdies | `GET /api/v1/course/holes/{hole}/credits?seasonId=...&limit=20` |
| Read the canonical rulebook | `GET /api/v1/rules` (returns `markdown`, `version`, and `status`) |

The feed accepts `teamId`, `playerId`, and `hole` filters. `limit` defaults to 20 and is capped at 100. Continue by sending the returned `nextCursor` as the URL-encoded `cursor`, keeping the same season and filters; stop at `null`. Hole history uses the same pagination and accepts holes 1–18. Refetch displayed pages to reflect corrections and voids. Feed order uses immutable publication time, not playing date or official claim order.

## Course and rules

The [canonical rulebook](/rules.md) governs the game. White is the standard tee; Gold is a researched reference scorecard and does not establish the unconfirmed forward-tee designation for players 58 and older. The course is maintained in source and has no write API.

The official group chat is the record of declarations and claims. API records summarize verified activity; they do not replace that chat or satisfy reporting and video requirements by themselves. A team awaiting confirmation has not yet been confirmed as the winner. Season dates, fees, and other unset rules must not be invented. The service has no tee-time booking or payment-processing API.

## Waitlist interest

Only submit a person's contact information when they have asked to join the waitlist. `POST /api/v1/waitlist` requires no account but does require `Content-Type: application/json`, `Origin: https://navygolf.app`, and `X-Birdie-CSRF: 1`. Its body is:

```json
{
  "name": "User-provided name",
  "phone": "+1 (410) 555-0199",
  "email": "user@example.com",
  "navalAcademyMember": false
}
```

Replace the examples with the user's details. Name, phone, and email are required. The optional membership flag defaults to false and does not restrict eligibility. Successful submissions return `{ "success": true }`, including repeated emails. Repeats retain the original details; they cannot update a contact or reveal whether someone has already signed up. Waitlist interest does not create an account, reserve a place, or enroll a player. Contact details are private and available only to authenticated administrators.

## Administrator authentication

Administration is available through the API to provisioned administrators and commissioners. There is no web sign-in interface, public account registration, or public password-reset workflow. Use an existing account supplied by the authorized user; an operator provisions or recovers accounts outside this public API.

1. Send `POST /api/auth/sign-in/email` with `Content-Type: application/json`, the canonical `Origin: https://navygolf.app`, and JSON `{ "email": "...", "password": "..." }`. Keep credentials private. Preserve every `Set-Cookie` response in a private cookie jar; the response body also contains sensitive session information.
2. Include those cookies on protected calls. `GET /api/v1/session` returns the authenticated user and role, or `null`. Preserve renewed cookies from subsequent responses. HTTPS uses `__Secure-better-auth.session_token`; local HTTP uses `better-auth.session_token`. Sessions expire after seven days and may renew after one day.
3. For **every game mutation**, include cookies, `Origin: https://navygolf.app`, `X-Birdie-CSRF: 1`, and `Content-Type: application/json`. Use the exact method and body from the [OpenAPI schema](/api/openapi.json). Waitlist submission is the only public game mutation and uses the same Origin and CSRF headers without a session.
4. Sign out with `POST /api/auth/sign-out`, cookies, the canonical Origin, `Content-Type: application/json`, and `{}`. Remove the local cookie jar afterward.

Do not put credentials or cookies in URLs, command arguments, source files, or public output. Cookie-based sessions are the supported mechanism; there is no API-key sign-up flow. In a server-side agent client, set the Origin explicitly; a browser's cross-origin behavior is not a substitute for these authentication requirements.

## Authorized administration workflow

Read current records before changing them. `GET /api/v1/players` lists all player identities only for administrators, including unrostered witnesses. The public leaderboard and team endpoints provide rostered public identities.

- `PUT /api/v1/season`, `/teams`, `/players`, and `/roster` maintain settings and identities. Include the existing ID to update; omitting an optional ID creates a record. Roster changes affecting credits require commissioner review and an explanatory reason.
- `POST /api/v1/rounds` records the pre-play chat declaration using `seasonId`, `teamId`, `playerId`, `playedOn`, `declarationReference`, and an outcome (`declared`, `completed`, `suspended`, or `incomplete`). The API enforces one round per player per day and six per calendar month, including rounds without birdies. `GET /api/v1/rounds` supplies IDs and versions. Update an outcome or reference using `PATCH /api/v1/rounds/{id}` with its current version and a reason.
- `POST /api/v1/credits` requires the matching `roundId` for a completed round or the Golf Shop suspension exception, plus an identified `witnessPlayerId` and `witnessPlayingPartner: true`. Verify captain approval, same-day chat reporting, and other rule requirements before submitting. Consult the live rulebook for witness eligibility.
- `GET /api/v1/credits/{id}` supplies a private credit's current version. `PATCH /api/v1/credits/{id}` corrects it; `POST /api/v1/credits/{id}/void` voids it. Both require that version and a reason. On a conflict, refetch and reconcile before attempting a revised change.
- Only a commissioner may call `POST /api/v1/winner`, after verifying every game requirement, including chat evidence and video. Calculated completion alone does not confirm a winner.
- `GET /api/v1/history`, `/export`, and `/waitlist` are private administrator reads. Waitlist pagination uses `limit` (default 100, maximum 200) and `offset` (default 0); continue with `nextOffset` until it is `null`.

## Responses and safe retries

No game operation changes records on GET. API responses, including public live reads, use `private, no-store`; retrieve fresh records when answering current-state questions. Public projections exclude private contact details, declaration evidence, notes, payment records, audit reasons, and authentication records.

Handle `400` as invalid input, `401` as missing or expired administrator authentication, `403` as an authorization or Origin/CSRF failure, `404` as an unknown resource, and `409` as a state or version conflict. Report server failures honestly. Do not blindly retry a write after an uncertain network result: first read the relevant records and establish whether it succeeded. There is no general idempotency-key interface.

The same procedures also support typed oRPC calls at `/api/rpc/*`. For a general HTTP agent, use the documented REST interface. See [llms.txt](/llms.txt) for when-to-use guidance and the [sitemap](/sitemap.xml) for public pages.
