{
    "openapi": "3.1.0",
    "info": {
        "title": "Scorsync API",
        "version": "1.0.0",
        "summary": "Live and final gymnastics competition results from Scorsync.",
        "description": "Scorsync publishes live and final competition results for gymnastics meets \u2014 USA Gymnastics\ntrampoline, double mini and tumbling, plus rhythmic and acrobatic gymnastics.\n\nAccess is scoped. Public reads need no credential. Coach-authorized clients use OAuth 2.0\nauthorization code with PKCE and receive a bearer token carrying only the scopes the coach\nconsented to. Personal access tokens carry an explicit list of abilities, and each endpoint\nchecks the ability it needs. Partner and meet-day integrations use dedicated header tokens\nthat unlock only their own surface.\n\nHuman-readable documentation: https://scorsync.com/developers",
        "contact": {
            "name": "Scorsync support",
            "email": "support@scorsync.com",
            "url": "https://scorsync.com/developers"
        }
    },
    "servers": [
        {
            "url": "https://scorsync.com",
            "description": "Production"
        }
    ],
    "externalDocs": {
        "description": "Scorsync developer resources",
        "url": "https://scorsync.com/developers"
    },
    "tags": [
        {
            "name": "Search",
            "description": "Public athlete, meet and club lookups."
        },
        {
            "name": "Releases",
            "description": "Published Scorsync release notes."
        },
        {
            "name": "OAuth",
            "description": "Coach-authorized access via OAuth 2.0 with PKCE."
        },
        {
            "name": "Partner integrations",
            "description": "Server-to-server reads for approved partners."
        },
        {
            "name": "Tokens",
            "description": "Personal access tokens and their abilities."
        },
        {
            "name": "MCP",
            "description": "Model Context Protocol server for AI agents."
        }
    ],
    "components": {
        "securitySchemes": {
            "coachOAuth": {
                "type": "oauth2",
                "description": "Coach-authorized access. The coach approves a consent screen listing the requested scopes and,\nwhen they are affiliated with more than one club, picks the club the grant applies to.\nPKCE is mandatory (`code_challenge_method=S256`) and `redirect_uri` must exactly match a\nregistered value. Clients are registered out of band \u2014 there is no dynamic registration.\n\nThe issued access token is a bearer token whose abilities are exactly the granted scopes, so a\nclient that asks for `profile` alone can never read a roster. Tokens do not expire and there is\nno refresh grant; disconnecting revokes the token instead.\n\nAuthorization server metadata: https://scorsync.com/.well-known/oauth-authorization-server",
                "flows": {
                    "authorizationCode": {
                        "authorizationUrl": "https://scorsync.com/oauth/authorize",
                        "tokenUrl": "https://scorsync.com/api/oauth/token",
                        "scopes": {
                            "profile": "The coach's name, email address and club affiliation. Always granted \u2014 it is the minimum needed to identify the account.",
                            "club.roster": "The athletes on the roster of the club the coach selected during consent.",
                            "results.read": "Competition results for the athletes on that roster."
                        }
                    }
                }
            },
            "personalAccessToken": {
                "type": "http",
                "scheme": "bearer",
                "description": "A Scorsync personal access token, sent as `Authorization: Bearer <token>`.\n\nEvery token is minted with an explicit list of abilities and endpoints check the ability they\nrequire rather than accepting any valid token. `POST /api/sanctum/token` issues a token for the\nsigned-in user; the MCP server requires a token holding the `mcp` ability."
            },
            "partnerServiceToken": {
                "type": "apiKey",
                "in": "header",
                "name": "X-Gympass-Token",
                "description": "A per-partner shared secret for the server-to-server endpoints under `/api/integrations`.\n\nIsolated from every other credential so it can be rotated on its own, and it unlocks nothing\noutside `/api/integrations`. A missing header is 401; a wrong one is 403."
            },
            "meetToken": {
                "type": "apiKey",
                "in": "header",
                "name": "X-Meet-Token",
                "description": "A token issued for one competition, used by scoring systems and meet-day boards.\n\nIt authorizes work on the meet it was issued for and nothing else: the token is matched against\nthe meet in the URL on every request."
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "properties": {
                    "message": {
                        "type": "string"
                    }
                }
            },
            "OAuthError": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "enum": [
                            "invalid_client",
                            "invalid_grant",
                            "invalid_request",
                            "unsupported_grant_type"
                        ]
                    }
                }
            },
            "TokenResponse": {
                "type": "object",
                "properties": {
                    "access_token": {
                        "type": "string"
                    },
                    "token_type": {
                        "type": "string",
                        "const": "Bearer"
                    },
                    "scope": {
                        "type": "string",
                        "description": "Space-delimited list of the scopes actually granted."
                    }
                }
            },
            "PaginatedEnvelope": {
                "type": "object",
                "description": "A Laravel length-aware paginator: the rows under `data`, with `current_page`, `last_page`, `per_page` and `total` alongside.",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object"
                        }
                    }
                }
            }
        }
    },
    "security": [],
    "paths": {
        "/api/athletes/search": {
            "get": {
                "tags": [
                    "Search"
                ],
                "summary": "Search athletes by name",
                "description": "Public. Matches the query against first name, last name and full name. Birthdates are never returned.",
                "security": [],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": false,
                        "description": "Substring to match. An empty query matches everything.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "include_club",
                        "in": "query",
                        "required": false,
                        "description": "Include each athlete's club in the response.",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number, starting at 1.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A paginated list of athletes.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedEnvelope"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/meets/search": {
            "get": {
                "tags": [
                    "Search"
                ],
                "summary": "Search meets by title",
                "description": "Public. Only meets that are visible, not hidden and not canceled are returned.",
                "security": [],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": false,
                        "description": "Substring to match. An empty query matches everything.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number, starting at 1.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A paginated list of meets.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedEnvelope"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/clubs/search": {
            "get": {
                "tags": [
                    "Search"
                ],
                "summary": "Search clubs by name",
                "description": "Public. Only clubs with a USAG club number are returned.",
                "security": [],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": false,
                        "description": "Substring to match. An empty query matches everything.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number, starting at 1.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A paginated list of clubs.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedEnvelope"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/whats-new": {
            "get": {
                "tags": [
                    "Releases"
                ],
                "summary": "List published releases",
                "description": "Public. Newest first.",
                "security": [],
                "parameters": [
                    {
                        "name": "per_page",
                        "in": "query",
                        "required": false,
                        "description": "Results per page. Capped at 50.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 50,
                            "default": 10
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number, starting at 1.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A paginated list of releases.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedEnvelope"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/whats-new/latest": {
            "get": {
                "tags": [
                    "Releases"
                ],
                "summary": "Get the latest published release",
                "security": [],
                "responses": {
                    "200": {
                        "description": "The most recent published release.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No release has been published.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/oauth/authorize": {
            "get": {
                "tags": [
                    "OAuth"
                ],
                "summary": "Authorization endpoint",
                "description": "Renders the consent screen. Session-authenticated: an unauthenticated coach is sent through\nlogin and returned here. On approval the browser is redirected to\n`redirect_uri?code=\u2026&state=\u2026`; on refusal to `redirect_uri?error=access_denied&state=\u2026`.\nCodes are single-use, hashed at rest and expire after ten minutes.",
                "security": [],
                "parameters": [
                    {
                        "name": "response_type",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "const": "code"
                        }
                    },
                    {
                        "name": "client_id",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "redirect_uri",
                        "in": "query",
                        "required": true,
                        "description": "Must exactly match a registered redirect URI.",
                        "schema": {
                            "type": "string",
                            "format": "uri"
                        }
                    },
                    {
                        "name": "scope",
                        "in": "query",
                        "required": false,
                        "description": "Space-delimited scopes. Unknown scopes are dropped rather than rejected.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "state",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "code_challenge",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "code_challenge_method",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "const": "S256"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The consent screen."
                    },
                    "302": {
                        "description": "Redirect back to the client with a code or an error."
                    }
                }
            }
        },
        "/api/oauth/token": {
            "post": {
                "tags": [
                    "OAuth"
                ],
                "summary": "Token endpoint",
                "description": "Exchanges an authorization code for a scoped bearer token. Form-encoded; `client_id` and\n`client_secret` go in the body rather than in Basic auth.\n\nEvery failure is a 400 with an OAuth error code, so a client can tell a spent or forged grant\n(re-drive consent) apart from a degraded service (retry later).",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "grant_type",
                                    "code",
                                    "redirect_uri",
                                    "code_verifier",
                                    "client_id",
                                    "client_secret"
                                ],
                                "properties": {
                                    "grant_type": {
                                        "type": "string",
                                        "const": "authorization_code"
                                    },
                                    "code": {
                                        "type": "string"
                                    },
                                    "redirect_uri": {
                                        "type": "string",
                                        "format": "uri"
                                    },
                                    "code_verifier": {
                                        "type": "string",
                                        "description": "The PKCE verifier for the challenge sent to /oauth/authorize."
                                    },
                                    "client_id": {
                                        "type": "string"
                                    },
                                    "client_secret": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A scoped bearer token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TokenResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "invalid_client, invalid_grant, invalid_request or unsupported_grant_type.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/OAuthError"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/oauth/userinfo": {
            "get": {
                "tags": [
                    "OAuth"
                ],
                "summary": "Identify the coach behind the token",
                "description": "Requires the `profile` scope. The club reported here is the one recorded on the token, re-checked against the coach's current verified affiliations on every call.",
                "security": [
                    {
                        "coachOAuth": [
                            "profile"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The coach behind the token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The token is missing, revoked or invalid.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The token does not carry the profile scope.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/me/club/athletes": {
            "get": {
                "tags": [
                    "OAuth"
                ],
                "summary": "The roster of the club on the token",
                "description": "Requires the `club.roster` scope. Always scoped to the club recorded on the token; `club_number` is a cross-check and a mismatch is a 403 rather than being quietly served.",
                "security": [
                    {
                        "coachOAuth": [
                            "club.roster"
                        ]
                    }
                ],
                "parameters": [
                    {
                        "name": "club_number",
                        "in": "query",
                        "required": false,
                        "description": "USAG club number the caller believes the token is for.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The athletes on the roster.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The token is missing, revoked or invalid.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The token lacks the club.roster scope, or club_number does not match the token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The connection has no verified club.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/integrations/athletes/{usagNumber}": {
            "get": {
                "tags": [
                    "Partner integrations"
                ],
                "summary": "Find an athlete by USAG member number",
                "security": [
                    {
                        "partnerServiceToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "usagNumber",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The athlete.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The service token header is missing.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The service token is wrong.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No athlete has that USAG number.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/integrations/athletes/search": {
            "get": {
                "tags": [
                    "Partner integrations"
                ],
                "summary": "Search athletes by name, optionally within one club",
                "security": [
                    {
                        "partnerServiceToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": false,
                        "description": "Substring to match. An empty query matches everything.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "club_number",
                        "in": "query",
                        "required": false,
                        "description": "Restrict to one USAG club number.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number, starting at 1.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A paginated list of athletes.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedEnvelope"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The service token header is missing.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The service token is wrong.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/integrations/athletes/{usagNumber}/results": {
            "get": {
                "tags": [
                    "Partner integrations"
                ],
                "summary": "Competition results for one athlete",
                "security": [
                    {
                        "partnerServiceToken": []
                    }
                ],
                "parameters": [
                    {
                        "name": "usagNumber",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The athlete's results.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The service token header is missing.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The service token is wrong.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The query parameters failed validation.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/sanctum/token": {
            "post": {
                "tags": [
                    "Tokens"
                ],
                "summary": "Issue a personal access token",
                "description": "Exchanges account credentials for a bearer token bound to the named device.",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "email",
                                    "password",
                                    "device_name"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "password": {
                                        "type": "string",
                                        "format": "password"
                                    },
                                    "device_name": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The issued token and the account it belongs to.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The credentials are incorrect or a field is missing.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/mcp/admin": {
            "post": {
                "tags": [
                    "MCP"
                ],
                "summary": "Model Context Protocol endpoint",
                "description": "A streamable-HTTP MCP server exposing Scorsync diagnostics and operations to AI clients.\nRequires a bearer token holding the `mcp` ability; a token without it is rejected even when\nit is otherwise valid. Tools are tiered read / write / destructive, write tools need an\nexplicit confirmation argument, destructive tools take a two-call confirmation token, and\nevery invocation is written to an audit log.",
                "security": [
                    {
                        "personalAccessToken": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "description": "A JSON-RPC 2.0 message as defined by the Model Context Protocol.",
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A JSON-RPC response or an event stream."
                    },
                    "401": {
                        "description": "The token is missing, revoked or lacks the mcp ability.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}