{"spec_version":"1.0.0","updated":"2026-07-05","license":"CC0 (the model itself)","read_first":"Entities are WHERE facts attach; Moments (see /api/v1/ontology) are the facts. Every write is a moment; entities are what moments reference.","entities":{"course":{"is":"A golf facility in the open dataset. The anchor of all geography.","id":"course_id (uuid)","parent":"place (a course IS a place of kind=course; place_id generalizes to ranges/sims/venues)","children":"holes (embedded), features (spatial polygons: greens/bunkers/water), tees","read":"GET /api/v1/courses/search · GET /api/v1/courses/{id} (free, ODbL)"},"hole":{"is":"One hole of a course: number, par, yardages per tee, and stroke_index.","terminology_rule":"The hole's difficulty allocation is ALWAYS `stroke_index` (1 = hardest). NEVER call it handicap_index — that term is reserved for a PLAYER's index. This distinction is normative."},"player":{"is":"A person who plays. Identified two ways, with a hard rule:","rule_pseudonym":"On raw moment writes, `player_id` MAY be any stable pseudonym you choose ('p1', 'alice') — scoring works entirely on pseudonyms.","rule_identity":"Identity-anchored reads (passport, wallet, profile, handicap, awards) REQUIRE the OpenGolf ID form: `ogid_` + sha256(lower(trim(email)))[:16]. A pseudonym has no passport.","upgrade_path":"complete_sign_in / sign_in_with_opengolf mints the ogid; write with it from then on and history anchors to the identity."},"session":{"is":"ONE scoring context — canonically, one round of golf. THE core container. All score/breadcrumb/side-game moments carry its session_id.","rule":"`round` is an informal synonym used in prose; it is NEVER a wire object. When you read \"round\", bind it to session_id.","reads":"GET /api/v1/sessions/{id}/scorecard · /leaderboard · /synthesize (live_state) · /feed (broadcast)"},"competition":{"is":"A session WITH a format contract (stroke|stableford|skins|nassau|match_play|… 15 kernels) and a lifecycle: open → closed → finalized (→ playoff supersede).","rule":"A competition IS-A session: its competition_id doubles as the session_id for its moments. `attempt` is the competition-context name for a score/side_game moment — same record, not a new object.","writes":"POST /api/v1/competitions · /{id}/attempt · /{id}/finalize · /{id}/playoff"},"event":{"is":"A multi-round tournament (OpenEvent): the parent of N sessions (its rounds), with field, divisions, draw, bracket, rolled leaderboards, and an event-level settle.","hierarchy":"event ⊃ sessions(=rounds); each session may be a competition."},"result":{"is":"The stored output of finalizing a competition (free, gross). Immutable with supersede lineage (corrections/playoffs create NEW results pointing back).","money_rule":"A result never pays. Money certainty is a separate, gated `settlement` (the notary): shown-as-money ≡ actually-owed (Wall 3)."},"award_asset":{"is":"Recognition objects. UNIFICATION STATEMENT: `award` (derived trophies, organizer honors) and `asset` (minted collectibles/coupons/memberships) are one recognition system with two mint paths; both appear in the trophy case. When building UI, read both."},"actor":{"is":"The generalized identity primitive: person | org | AI agent | system. A player is an actor of kind person. Orgs verify; agents act with keys bound to a subject."},"moment":{"is":"THE atomic fact (see /api/v1/ontology for all 42 types + payload schemas). Append-only, chain-notarized, idempotent by (contributor, dedup_key).","write":"POST /api/v1/moments (keyed) — one endpoint for every sensor, app, and agent."}},"choosing_the_score_pipeline":{"rule_of_thumb":"casual → score_round · organized → competition lifecycle · streaming/devices → raw moments","one_shot_round":"Use `compute` (POST /api/v1/compute/{format}) or the MCP `score_round` when you HAVE all the strokes and want standings NOW. Nothing stored unless you also submit moments. Best for: calculators, casual apps.","competition_lifecycle":"Use create_competition → record_attempt(s) → finalize when scores ARRIVE OVER TIME, from multiple people, with a format + tie_policy + immutable Result. Best for: leagues, tournaments, anything with standings that matter.","raw_moments":"Use submit_moment / POST /moments when you are a SENSOR or live app streaming facts (scores, GPS, presence) into a session; fold them later via /sessions/{id}/scorecard or a competition. Best for: trackers, watches, launch monitors."},"error_contract":{"envelope":"{ \"error\": \"<machine_code_or_message>\", ...context }","common":{"401":"no/invalid key on a keyed surface (writes, identity reads). Fix: mint a free key (/developer) or use the keyless read.","403":"valid key, missing grant (gated: geo/money/compute) or verification. Body names the gate.","404":"unknown id/route.","409":"state conflict (e.g. consuming a consumed voucher).","429":"rate limit; response tells you the reset. Limits are generous for free reads.","422_or_400":"validation_failed — body says which field.","idempotent_replay":"Replays of keyed writes with the same dedup_key return the ORIGINAL record + `idempotent_replay: true` — never an error, never a duplicate."}}}