{"openapi":"3.1.0","info":{"title":"Convocados API","version":"3.242.0","description":"API for organizing pickup sports games — manage events, players, teams, and more.","license":{"name":"MIT"}},"servers":[{"url":"https://convocados.cabeda.dev","description":"Production"},{"url":"http://localhost:4321","description":"Local development"}],"paths":{"/api/health":{"get":{"summary":"Health check","tags":["System"],"security":[],"responses":{"200":{"description":"Service is healthy"}}}},"/api/openapi.json":{"get":{"summary":"OpenAPI 3.1 specification (this document)","tags":["System"],"security":[],"responses":{"200":{"description":"OpenAPI document"}}}},"/api/events":{"get":{"summary":"List events (authenticated user's events)","tags":["Events"],"responses":{"200":{"description":"List of events"}}},"post":{"summary":"Create a new event","tags":["Events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","dateTime"],"properties":{"title":{"type":"string"},"location":{"type":"string"},"dateTime":{"type":"string","format":"date-time"},"maxPlayers":{"type":"integer","default":10},"sport":{"type":"string","default":"football-5v5"},"teamOneName":{"type":"string","default":"Ninjas"},"teamTwoName":{"type":"string","default":"Gunas"},"isRecurring":{"type":"boolean"},"recurrenceRule":{"type":"string"}}}}}},"responses":{"201":{"description":"Event created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/public":{"get":{"summary":"List upcoming public events (soonest first)","tags":["Events"],"security":[],"parameters":[{"name":"sport","in":"query","schema":{"type":"string"},"description":"Filter by sport"},{"name":"hasSpots","in":"query","schema":{"type":"boolean"},"description":"Only events with available spots"}],"responses":{"200":{"description":"List of upcoming public events"}}}},"/api/events/{id}":{"get":{"summary":"Get event details (anonymous link access; locked when password-protected)","tags":["Events"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Event details with players and teams"},"404":{"description":"Event not found"}}}},"/api/events/{id}/title":{"put":{"summary":"Update event title","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"}}}}}},"responses":{"200":{"description":"Title updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/location":{"put":{"summary":"Update event location","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"location":{"type":"string"}}}}}},"responses":{"200":{"description":"Location updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/sport":{"put":{"summary":"Update event sport","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Sport updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/recurrence":{"put":{"summary":"Set or clear an event's recurrence (post-game repeat / make it recurring)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isRecurring":{"type":"boolean"},"recurrenceFreq":{"type":"string","enum":["daily","weekly","monthly","yearly"]},"recurrenceInterval":{"type":"integer","minimum":1},"recurrenceByDay":{"type":"string"}},"required":["isRecurring"]}}}},"responses":{"200":{"description":"Recurrence updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/visibility":{"put":{"summary":"Toggle event public/private visibility","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Visibility updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/balanced":{"put":{"summary":"Toggle balanced teams mode","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Balanced mode updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/show-competitive-data":{"put":{"summary":"Toggle competitive data visibility","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Competitive data visibility updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/manual-rating":{"put":{"summary":"Toggle manual rating","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Manual rating updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/mvp-enabled":{"put":{"summary":"Toggle MVP voting","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"MVP voting updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/mvp-elo-enabled":{"put":{"summary":"Toggle MVP ELO bonus","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"MVP ELO updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/duration":{"put":{"summary":"Update game duration","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Duration updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/datetime":{"put":{"summary":"Update event date and time","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dateTime":{"type":"string"},"timezone":{"type":"string"}}}}}},"responses":{"200":{"description":"Date/time updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/elo":{"put":{"summary":"Toggle ELO ratings for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"ELO setting updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/competition":{"put":{"summary":"Update Competition settings (master enable, rank decay, inactive behavior)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"rankDecayEnabled":{"type":"boolean"},"inactiveRankBehavior":{"type":"string","enum":["freeze","reset"]}}}}}},"responses":{"200":{"description":"Competition settings updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"409":{"description":"Locked while a Season is active or under review"}}}},"/api/events/{id}/seasons":{"get":{"summary":"List Seasons for an event","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Seasons list"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/rank":{"get":{"summary":"Get the Season Rank ladder for an event Season","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Season Rank ladder"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}":{"get":{"summary":"Get a Season's detail (members, crews, standings)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Season detail"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"patch":{"summary":"Update, activate, complete, cancel, or reopen a Season","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Season updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/membership":{"post":{"summary":"Join a Season","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Joined"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Leave a Season","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Left"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/memberships":{"post":{"summary":"Add a Season member (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Member added"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/memberships/bulk":{"post":{"summary":"Bulk-enroll recent players as Season members (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Members added"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/memberships/candidates":{"get":{"summary":"List candidate players for a Season (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Candidates"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/memberships/{membershipId}":{"delete":{"summary":"Remove a Season member (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"},{"name":"membershipId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Member removed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/crew-proposals":{"get":{"summary":"List Season Crew proposals and eligible candidates","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Proposals and candidates"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"post":{"summary":"Submit a Season Crew proposal","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"201":{"description":"Proposal created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/crew-proposals/{proposalId}":{"patch":{"summary":"Approve or reject a Season Crew proposal (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"},{"name":"proposalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Proposal reviewed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/crews":{"post":{"summary":"Save Season Crews (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Crews saved"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/crews/recommend":{"post":{"summary":"Recommend balanced Season Crews (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"}],"responses":{"200":{"description":"Crews recommended"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/seasons/{seasonId}/crews/{crewId}":{"delete":{"summary":"Delete a Season Crew (admin)","tags":["Seasons"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"seasonId","in":"path","required":true,"schema":{"type":"string"},"description":"Season ID"},{"name":"crewId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Crew deleted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/hide-elo-in-teams":{"put":{"summary":"Toggle hiding ELO in team view","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Hide ELO setting updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/split-costs":{"put":{"summary":"Toggle split costs for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Split costs setting updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/cost":{"get":{"summary":"Get event cost","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Event cost"},"404":{"description":"Event not found"}}}},"/api/events/{id}/cost/override":{"put":{"summary":"Override player cost amount","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Cost override saved"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/follow":{"get":{"summary":"Get follow status for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Follow status"},"401":{"description":"Unauthorized"}}},"post":{"summary":"Follow an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Followed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"put":{"summary":"Update follow notification overrides","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Follow updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Unfollow an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Unfollowed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/invitation-opt-out":{"post":{"summary":"Toggle per-event invite opt-out (ADR 0025)","description":"While set, RSVP pings, recruitment pings, suggestions and PlayerInvite creation are suppressed for this event. Reversible; cleared automatically when the caller rejoins the player list.","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["optOut"],"properties":{"optOut":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Opt-out state updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/rsvp":{"post":{"summary":"Submit RSVP (yes/no/maybe) for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["yes","no","maybe"]}}}}}},"responses":{"200":{"description":"RSVP submitted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/leave":{"post":{"summary":"Leave the event as the authenticated player (archives Player, sets RSVP no, unfollows)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Left the event"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/no-show":{"post":{"summary":"Mark a player as no-show for a game (owner/admin)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"},"eventPlayerId":{"type":"string"},"noShow":{"type":"boolean"}}}}}},"responses":{"200":{"description":"No-show updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/access":{"put":{"summary":"Set or remove event password","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Access updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/access/verify":{"post":{"summary":"Verify event password","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Password verified"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/archive":{"put":{"summary":"Archive or unarchive an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["archive"],"properties":{"archive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Event archive state updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/post-game-status":{"get":{"summary":"Get post-game status (voting, MVP)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Post-game status"}}}},"/api/events/{id}/max-players":{"put":{"summary":"Update max players for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Max players updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/attendance":{"get":{"summary":"Get attendance stats for an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Attendance data"}}}},"/api/events/{id}/payments":{"get":{"summary":"Get payment status for event players","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Payment statuses"}}},"put":{"summary":"Update a player's payment status","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Payment updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/bulk":{"put":{"summary":"Bulk mark all pending/sent payments as paid","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"All payments marked as paid"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/settlement":{"get":{"summary":"People-first settlement summary (receivers public, debtor names owner/admin-only)","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Settlement summary"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"put":{"summary":"Settle one share (owner/admin)","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"},"eventPlayerId":{"type":"string"}},"required":["gameId","eventPlayerId"]}}}},"responses":{"200":{"description":"Share settled"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Revert a settled share (owner/admin)","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"},"eventPlayerId":{"type":"string"}},"required":["gameId","eventPlayerId"]}}}},"responses":{"200":{"description":"Share reverted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/settlement/bulk":{"put":{"summary":"Settle all unpaid shares for a game (owner/admin)","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"}},"required":["gameId"]}}}},"responses":{"200":{"description":"Shares settled"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/settlement/self-report":{"post":{"summary":"Self-report that a player sent their share","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"},"eventPlayerId":{"type":"string"}},"required":["gameId","eventPlayerId"]}}}},"responses":{"200":{"description":"Self-report recorded"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/payer-check-in/mark-all-paid":{"post":{"summary":"Payer answers the 24h check-in: everyone paid — settle all unpaid shares of a game","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"}},"required":["gameId"]}}}},"responses":{"200":{"description":"Shares settled"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/payments/payer-check-in/snooze":{"post":{"summary":"Payer answers the 24h check-in: ask again in 24h","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string"}},"required":["gameId"]}}}},"responses":{"200":{"description":"Check-in snoozed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/balance":{"get":{"summary":"Get outstanding balance and enforcement info for the caller","tags":["Payments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Balance, aggregate, enforcement level"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/log":{"get":{"summary":"Get event activity log","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Paginated activity log"}}}},"/api/events/{id}/history/{historyId}/mvp-vote":{"post":{"summary":"Cast MVP vote for a game","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Vote cast"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/history/{historyId}/mvp":{"get":{"summary":"Get MVP results for a game","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"MVP results"}}}},"/api/events/{id}/history/{historyId}/match-events":{"get":{"summary":"List a game's match events and the score derived from its goals","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Match event timeline and derived score"}}},"post":{"summary":"Log a match event (goal or assist) on a settled game","description":"count records how many goals one entry stands for (\"X scored 3\" is a single row with count=3). Omit it for a single goal.","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Match event created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/history/{historyId}/match-events/{matchEventId}":{"delete":{"summary":"Remove a match event from a settled game","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}},{"name":"matchEventId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Match event removed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/match-stats":{"get":{"summary":"Get the per-player scorer table for an event, replayed from its match events","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Top scorers"},"404":{"description":"Event not found"}}}},"/api/users/{id}/stats":{"get":{"summary":"Get a user's game statistics (respects profile visibility)","tags":["Users"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User ID"}],"responses":{"200":{"description":"Player statistics"},"404":{"description":"User not found"}}}},"/api/events/{id}/team-names":{"put":{"summary":"Update team names","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Team names updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/claim":{"post":{"summary":"Claim ownership of an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Ownership claimed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Relinquish ownership of an event","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Ownership relinquished"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/transfer":{"post":{"summary":"Transfer event ownership","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Ownership transferred"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/adopt":{"post":{"summary":"Adopt an Open Pickup (ADR-0021)","tags":["Events"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Pickup adopted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/status":{"get":{"summary":"Get event status (player count, spots left)","tags":["Events"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Event status"}}}},"/api/events/{id}/players":{"post":{"summary":"Add a player to the event","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional client-generated UUID. Replays the cached 2xx response on retry with the same key + same body. Reusing the key with a different payload returns 422. See ADR 0012.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"linkToAccount":{"type":"boolean"},"email":{"type":"string"}}}}}},"responses":{"200":{"description":"Player added"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"},"422":{"description":"Idempotency-Key reused with different payload"}}},"delete":{"summary":"Remove a player from the event","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Player removed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/reorder-players":{"put":{"summary":"Reorder players (owner only)","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Players reordered"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/claim-player":{"post":{"summary":"Claim an anonymous player as your account","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Player claimed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/undo-remove":{"post":{"summary":"Undo a recent player removal","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Removal undone"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/known-players":{"get":{"summary":"Get known player names for autocomplete","tags":["Players"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"List of known player names"}}}},"/api/events/{id}/teams":{"get":{"summary":"Get current team assignments","tags":["Teams"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Team assignments"}}},"patch":{"summary":"Manually update team assignments","tags":["Teams"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Teams updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/randomize":{"post":{"summary":"Randomize team assignments","tags":["Teams"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Teams randomized"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/history":{"get":{"summary":"Get game history for an event","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Game history entries"}}}},"/api/events/{id}/history/{historyId}":{"get":{"summary":"Get a single history entry","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"History entry"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"patch":{"summary":"Update a history entry (score, status)","tags":["History"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"historyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"History entry updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/ratings":{"get":{"summary":"Get player ELO ratings for an event","tags":["Ratings"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Player ratings"}}},"patch":{"summary":"Set a player's initial rating","tags":["Ratings"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"initialRating":{"type":"integer"}},"required":["name","initialRating"]}}}},"responses":{"200":{"description":"Initial rating set"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/ratings/recalculate":{"post":{"summary":"Recalculate all ELO ratings from history","tags":["Ratings"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Ratings recalculated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/purge-player":{"delete":{"summary":"Delete a player and their rating history","tags":["Ratings"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"Player purged"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/calendar":{"get":{"summary":"Download .ics calendar file for an event","tags":["Calendar"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"iCalendar file","content":{"text/calendar":{}}}}}},"/api/events/{id}/calendar.ics":{"get":{"summary":"iCal feed for an event (token-authenticated)","tags":["Calendar"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"iCalendar feed"},"401":{"description":"Missing token"},"403":{"description":"Invalid token"}}}},"/api/users/{id}/calendar.ics":{"get":{"summary":"iCal feed for a user's games (token-authenticated)","tags":["Calendar"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User ID"},{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"iCalendar feed"},"401":{"description":"Missing token"},"403":{"description":"Invalid token"}}}},"/api/events/{id}/push":{"post":{"summary":"Subscribe to push notifications for an event","tags":["Push"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Subscribed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Unsubscribe from push notifications","tags":["Push"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Unsubscribed"}}}},"/api/push/vapid-public-key":{"get":{"summary":"Get VAPID public key for push subscriptions","tags":["Push"],"security":[],"responses":{"200":{"description":"VAPID public key"}}}},"/api/push/test":{"post":{"summary":"Send a self-test push to the caller's web subscriptions","description":"Used by the push-prompt banner to confirm the channel works. Returns the number of subscriptions that accepted the test.","tags":["Push"],"responses":{"200":{"description":"Test push sent","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"delivered":{"type":"integer","description":"Number of subscriptions that accepted the push"},"total":{"type":"integer","description":"Total subscriptions attempted"}}}}}},"401":{"description":"Not authenticated"}}}},"/api/events/{id}/webhooks":{"get":{"summary":"List webhooks for an event","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"List of webhooks"}}},"post":{"summary":"Register a webhook for an event","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string"},"events":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Webhook created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/webhooks/{webhookId}":{"patch":{"summary":"Update which events a webhook receives","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["events"],"properties":{"events":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Webhook updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Delete a webhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook deleted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/users/{id}":{"get":{"summary":"Get user profile (email only on your own profile)","tags":["Users"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User ID"}],"responses":{"200":{"description":"User profile with game history"},"404":{"description":"User not found"}}},"patch":{"summary":"Update own profile","tags":["Users"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Profile updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/me/games":{"get":{"summary":"Get authenticated user's games","tags":["Users"],"responses":{"200":{"description":"Owned and joined games"},"401":{"description":"Unauthorized"}}}},"/api/me/home":{"get":{"summary":"Signed-in Home feed: next games (Up next) + a glimpse of joinable public games (Discover)","tags":["Users"],"responses":{"200":{"description":"Up next + Discover"},"401":{"description":"Unauthorized"}}}},"/api/me/app-open":{"post":{"summary":"Record a native app-open heartbeat (idempotent per UTC day)","tags":["Users"],"responses":{"200":{"description":"Recorded"},"401":{"description":"Unauthorized"}}}},"/api/me/co-players":{"get":{"summary":"List people the caller has co-played with (all events, ranked by frequency)","tags":["Users"],"responses":{"200":{"description":"Co-play list"},"401":{"description":"Unauthorized"}}}},"/api/me/locations":{"get":{"summary":"Venues the caller plays at, most frequent first (create-game location suggestions)","tags":["Users"],"responses":{"200":{"description":"Ranked venue list"},"401":{"description":"Unauthorized"}}}},"/api/places":{"get":{"summary":"Place lookup for location pickers: autocomplete (?q=) or reverse geocode (?lat=&lng=)","tags":["Events"],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"lat","in":"query","required":false,"schema":{"type":"number"}},{"name":"lng","in":"query","required":false,"schema":{"type":"number"}}],"responses":{"200":{"description":"Ranked suggestions or a place name"},"401":{"description":"Unauthorized"}}}},"/api/me/stats":{"get":{"summary":"Get authenticated user's statistics","tags":["Users"],"responses":{"200":{"description":"Player statistics"},"401":{"description":"Unauthorized"}}}},"/api/me/profile":{"get":{"summary":"Get authenticated user's profile","tags":["Users"],"responses":{"200":{"description":"User profile"},"401":{"description":"Unauthorized"}}},"put":{"summary":"Update authenticated user's profile","tags":["Users"],"responses":{"200":{"description":"Profile updated"},"401":{"description":"Unauthorized"}}}},"/api/me/photo":{"post":{"summary":"Set the authenticated user's profile photo (base64 data URL)","tags":["Users"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["image"],"properties":{"image":{"type":"string","description":"data:image/(jpeg|png|webp);base64,... up to 500 KB"}}}}}},"responses":{"200":{"description":"Photo updated"},"400":{"description":"Invalid or oversized image"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Clear the authenticated user's profile photo","tags":["Users"],"responses":{"200":{"description":"Photo removed"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/me/notification-preferences":{"get":{"summary":"Get notification preferences","tags":["Users"],"responses":{"200":{"description":"Notification preferences"},"401":{"description":"Unauthorized"}}},"put":{"summary":"Update notification preferences","tags":["Users"],"responses":{"200":{"description":"Preferences updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/me/calendar-token":{"post":{"summary":"Generate or retrieve a calendar feed token","tags":["Calendar"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["user","event"]},"eventId":{"type":"string"}}}}}},"responses":{"200":{"description":"Token and feed URL"},"401":{"description":"Unauthorized"}}},"delete":{"summary":"Revoke all calendar tokens","tags":["Calendar"],"responses":{"200":{"description":"Tokens revoked"},"401":{"description":"Unauthorized"}}}},"/api/me/credentials":{"get":{"summary":"List linked sign-in credentials for the session user","tags":["Users"],"responses":{"200":{"description":"Credentials (id, providerId, accountId, issuer, createdAt)"},"401":{"description":"Unauthorized"}}},"delete":{"summary":"Unlink a credential (blocked when it is the only one — ADR 0040)","tags":["Users"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["accountId"],"properties":{"accountId":{"type":"string","description":"Credential row id (not the provider account id)"}}}}}},"responses":{"200":{"description":"Credential unlinked"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/me/credentials/pending-merge":{"get":{"summary":"Get the pending cross-account merge waiting on interstitial confirm","tags":["Users"],"responses":{"200":{"description":"Pending merge details, or null"},"401":{"description":"Unauthorized"}}}},"/api/me/credentials/merge":{"post":{"summary":"Confirm the pending cross-account merge (ADR 0040)","tags":["Users"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirm"],"properties":{"confirm":{"type":"boolean","enum":[true]}}}}}},"responses":{"200":{"description":"Merge completed"},"400":{"description":"Confirmation required or no pending merge"},"401":{"description":"Unauthorized"},"500":{"description":"Merge failed"}}}},"/.well-known/openid-configuration":{"get":{"summary":"OpenID Connect discovery document","tags":["OAuth"],"security":[],"responses":{"200":{"description":"OIDC metadata"}}}},"/api/auth/oauth2/authorize":{"get":{"summary":"Authorization endpoint (OAuth 2.1 authorization code flow)","tags":["OAuth"],"parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"scope","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","required":true,"schema":{"type":"string"}},{"name":"code_challenge","in":"query","required":true,"schema":{"type":"string"},"description":"PKCE code challenge (S256)"},{"name":"code_challenge_method","in":"query","required":true,"schema":{"type":"string","enum":["S256"]}}],"responses":{"302":{"description":"Redirect to login or consent page"},"400":{"description":"Invalid request parameters"}}}},"/api/auth/oauth2/token":{"post":{"summary":"Token endpoint — exchange code for tokens or refresh","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"code":{"type":"string","description":"Authorization code (for authorization_code grant)"},"redirect_uri":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"code_verifier":{"type":"string","description":"PKCE code verifier"},"refresh_token":{"type":"string","description":"Refresh token (for refresh_token grant)"}}}}}},"responses":{"200":{"description":"Access token, refresh token, and optional ID token"},"400":{"description":"Invalid grant or request"},"401":{"description":"Invalid client credentials"},"429":{"description":"Rate limited"}}}},"/api/auth/oauth2/register":{"post":{"summary":"Dynamic client registration (RFC 7591)","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirect_uris"],"properties":{"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"client_name":{"type":"string"},"client_uri":{"type":"string","format":"uri"},"logo_uri":{"type":"string","format":"uri"},"scope":{"type":"string"},"token_endpoint_auth_method":{"type":"string","enum":["none","client_secret_basic","client_secret_post"]},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Registered client with client_id and optional client_secret"},"400":{"description":"Invalid registration request"},"429":{"description":"Rate limited (5 registrations per hour)"}}}},"/api/auth/oauth2/userinfo":{"get":{"summary":"UserInfo endpoint — returns claims about the authenticated user","tags":["OAuth"],"security":[{"oauth2":["openid"]}],"responses":{"200":{"description":"User claims (sub, email, name, picture)"},"401":{"description":"Invalid or missing bearer token"}}}},"/api/auth/oauth2/introspect":{"post":{"summary":"Token introspection (RFC 7662)","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"The token to introspect"}}}}}},"responses":{"200":{"description":"Token metadata with active status"},"429":{"description":"Rate limited"}}}},"/api/auth/oauth2/revoke":{"post":{"summary":"Token revocation (RFC 7009)","tags":["OAuth"],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"The token to revoke"}}}}}},"responses":{"200":{"description":"Token revoked (always returns 200 per RFC 7009)"},"429":{"description":"Rate limited"}}}},"/api/court-watches":{"get":{"summary":"List court watches","tags":["Court Finder"],"responses":{"200":{"description":"User's court watches"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"post":{"summary":"Create a court watch","tags":["Court Finder"],"responses":{"201":{"description":"Watch created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/court-watches/{id}":{"delete":{"summary":"Delete a court watch","tags":["Court Finder"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Watch deleted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/court-alternatives":{"get":{"summary":"Search court alternatives for an event","tags":["Court Finder"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Court alternatives"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/suggestions":{"get":{"summary":"Ranked co-play invite suggestions for the current user on an event","tags":["Invites"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Ranked suggestions"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/events/{id}/invites":{"get":{"summary":"List pending invites for the current game (owner/admin/player)","tags":["Invites"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Invites"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"post":{"summary":"Create a player invite","tags":["Invites"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Invite created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"delete":{"summary":"Retract a player invite","tags":["Invites"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"responses":{"200":{"description":"Invite retracted"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}},"patch":{"summary":"Resend a pending player invite","tags":["Invites"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Event ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"inviteId":{"type":"string"}},"required":["inviteId"]}}}},"responses":{"200":{"description":"Invite resent"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}}}},"/api/invite/{token}":{"get":{"summary":"Look up an invite by token (invite-by-link)","tags":["Invites"],"security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invite details"}}},"post":{"summary":"Respond to an invite (accept/decline)","tags":["Invites"],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invite accepted or declined"}}}},"/api/mcp":{"post":{"summary":"MCP 2026-07-28 stateless endpoint (tools/list, tools/call, server/discover)","tags":["MCP"],"security":[{"oauth2":[]}],"parameters":[{"name":"MCP-Protocol-Version","in":"header","required":true,"schema":{"type":"string","enum":["2026-07-28"]}},{"name":"Mcp-Method","in":"header","required":true,"schema":{"type":"string"}},{"name":"Mcp-Name","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string"},"id":{},"method":{"type":"string"},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"JSON-RPC result"},"400":{"description":"Header or method error"},"401":{"description":"Unauthorized"}}},"get":{"summary":"MCP SSE deprecation hint (use POST)","tags":["MCP"],"responses":{"405":{"description":"Use POST with Mcp-Method header"}}}},"/.well-known/oauth-protected-resource":{"get":{"summary":"OAuth protected resource metadata (RFC 9728) for MCP","tags":["OAuth"],"security":[],"responses":{"200":{"description":"Protected resource metadata"}}}},"/.well-known/oauth-authorization-server":{"get":{"summary":"OAuth authorization server metadata (RFC 8414 + RFC 9207 iss)","tags":["OAuth"],"security":[],"responses":{"200":{"description":"Authorization server metadata"}}}}},"tags":[{"name":"System","description":"Health and status"},{"name":"Events","description":"Create and manage game events"},{"name":"Players","description":"Add, remove, and manage players"},{"name":"Teams","description":"Team assignments and randomization"},{"name":"History","description":"Game history and scores"},{"name":"Ratings","description":"ELO player ratings"},{"name":"Calendar","description":"iCal feeds and calendar integration"},{"name":"Push","description":"Push notification subscriptions"},{"name":"Payments","description":"Event cost splitting and payment tracking"},{"name":"Webhooks","description":"Webhook subscriptions and delivery"},{"name":"Users","description":"User profiles and authentication"},{"name":"OAuth","description":"OAuth 2.1 authorization server endpoints"},{"name":"MCP","description":"Model Context Protocol 2026-07-28 stateless tools"}],"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"OAuth 2.1 authorization code flow with PKCE","flows":{"authorizationCode":{"authorizationUrl":"/api/auth/oauth2/authorize","tokenUrl":"/api/auth/oauth2/token","scopes":{"openid":"Verify your identity","profile":"View your basic profile","email":"View your email address","offline_access":"Stay signed in (refresh tokens)","read:profile":"View your profile","read:events":"View your events","write:events":"Modify event settings","create:events":"Create new events","manage:players":"Add and remove players","read:ratings":"View ELO ratings","read:history":"View game history","manage:teams":"Randomize and assign teams","manage:webhooks":"Manage webhooks","manage:push":"Manage push subscriptions","read:calendar":"Access calendar feeds","manage:payments":"Manage costs and payments"}}}},"apiKey":{"type":"apiKey","in":"header","name":"Authorization","description":"API key authentication. Use `Bearer cvk_...` format."}}},"security":[{"oauth2":[]},{"apiKey":[]}]}