{
  "openapi": "3.1.0",
  "info": {
    "title": "League of Spin Public API",
    "version": "1.0.0",
    "description": "Read-only access to the League of Spin database of outdoor table tennis spots, plus clubs, tournaments, and public player profiles.\n\nAll endpoints are read-only and need no authentication. Keep to roughly 120 GET requests per minute per IP.\n\nSpot locations combine OpenStreetMap data (© OpenStreetMap contributors, ODbL 1.0), municipal open-data imports, and community submissions with photos and reviews. When you cite or reuse spot data, attribute \"League of Spin (leagueofspin.com)\" and, for map-derived records, \"© OpenStreetMap contributors\". Read-only GET requests from agents are welcome at reasonable rates (about 120 requests/minute per IP). Contact: info@leagueofspin.com. Preferred citation: League of Spin — leagueofspin.com.",
    "contact": {
      "name": "League of Spin",
      "email": "info@leagueofspin.com",
      "url": "https://leagueofspin.com"
    },
    "license": {
      "name": "ODbL 1.0 for map-derived records — attribution required, see description",
      "url": "https://opendatacommons.org/licenses/odbl/1-0/"
    }
  },
  "servers": [
    {
      "url": "https://leagueofspin.com"
    }
  ],
  "externalDocs": {
    "description": "Plain-text site and API index for AI agents",
    "url": "https://leagueofspin.com/llms.txt"
  },
  "paths": {
    "/api/leagues": {
      "get": {
        "operationId": "getLeagues",
        "summary": "Player rankings for the global, country or city league, highest rating first",
        "description": "Player rankings for the global, country or city league, highest rating first. `country` accepts any spelling (\"USA\", \"United States\", \"Österreich\"); `city` accepts the canonical `cc#city` key or a bare city name. Page with `cursor`; `startRank` gives the rank of the first row.",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "global (default), country, or city",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Required for scope=country. Any spelling or ISO code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Required for scope=city. Canonical `cc#city` key, or a bare city name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Players per page, 1-100 (default 50)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from a previous response's nextCursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "players": [
                    {
                      "username": "smasher",
                      "eloRating": 1374,
                      "wins": 12,
                      "losses": 3,
                      "location": {
                        "city": "Berlin",
                        "country": "Germany"
                      }
                    }
                  ],
                  "startRank": 1,
                  "scope": "country",
                  "country": "de"
                }
              }
            }
          }
        }
      }
    },
    "/api/spots": {
      "get": {
        "operationId": "getSpots",
        "summary": "Search table-tennis spots by bounding box, center point, or text",
        "description": "Search table-tennis spots by bounding box, center point, or text. Returns at most 1500 spots (`truncated: true` when capped); when a viewport is empty, `nearest` points at the closest spot outside it.",
        "parameters": [
          {
            "name": "north",
            "in": "query",
            "required": false,
            "description": "Bounding-box north latitude (use with south/east/west)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "south",
            "in": "query",
            "required": false,
            "description": "Bounding-box south latitude",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "east",
            "in": "query",
            "required": false,
            "description": "Bounding-box east longitude",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "west",
            "in": "query",
            "required": false,
            "description": "Bounding-box west longitude",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "description": "Center latitude (use with lng and maxDistance)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lng",
            "in": "query",
            "required": false,
            "description": "Center longitude",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "maxDistance",
            "in": "query",
            "required": false,
            "description": "Radius in kilometers around lat/lng",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text filter on spot names",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "spots": [
                    {
                      "id": "abc123",
                      "slug": "monbijoupark-berlin",
                      "name": "Monbijoupark",
                      "lat": 52.5238,
                      "lng": 13.3966,
                      "city": "Berlin",
                      "country": "Germany",
                      "numTables": 3,
                      "url": "https://leagueofspin.com/spots/monbijoupark-berlin"
                    }
                  ],
                  "truncated": false
                }
              }
            }
          }
        }
      }
    },
    "/api/spots/{id}": {
      "get": {
        "operationId": "getSpotsById",
        "summary": "Single spot by slug or id, with nearby spots and its city page link",
        "description": "Single spot by slug or id, with nearby spots and its city page link.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Spot slug (preferred) or raw id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/weather": {
      "get": {
        "operationId": "getWeather",
        "summary": "Current outdoor playing conditions (wind, temperature, precipitation) for a coordinate; wind is the decisive signal for outdoor table tennis",
        "description": "Current outdoor playing conditions (wind, temperature, precipitation) for a coordinate; wind is the decisive signal for outdoor table tennis. Cached 30 minutes.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "description": "Longitude",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/clubs": {
      "get": {
        "operationId": "getClubs",
        "summary": "Table tennis clubs, filterable by city, country, or search text",
        "description": "Table tennis clubs, filterable by city, country, or search text.",
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Filter by city name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Filter by country name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text filter",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max results (default 50)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/clubs/rankings": {
      "get": {
        "operationId": "getClubsRankings",
        "summary": "Clubs ranked by average member ELO",
        "description": "Clubs ranked by average member ELO.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/tournaments": {
      "get": {
        "operationId": "getTournaments",
        "summary": "Tournaments, filterable by status",
        "description": "Tournaments, filterable by status.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "registration_open | in_progress | completed",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max results (default 20)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/tournaments/{id}": {
      "get": {
        "operationId": "getTournamentsById",
        "summary": "Tournament detail by id or slug",
        "description": "Tournament detail by id or slug.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Tournament id or slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/cities/{citySlug}": {
      "get": {
        "operationId": "getCitiesByCitySlug",
        "summary": "City centre point and spot count by slug — for re-centering a map on a named city (e.g",
        "description": "City centre point and spot count by slug — for re-centering a map on a named city (e.g. /api/cities/berlin).",
        "parameters": [
          {
            "name": "citySlug",
            "in": "path",
            "required": true,
            "description": "City slug (e.g. berlin)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "citySlug": "berlin",
                  "city": "Berlin",
                  "country": "Germany",
                  "countryCode": "de",
                  "spotCount": 42,
                  "centerLat": 52.52,
                  "centerLng": 13.405
                }
              }
            }
          }
        }
      }
    },
    "/api/profile/{username}": {
      "get": {
        "operationId": "getProfileByUsername",
        "summary": "Public player profile by username: ELO, record, playing style",
        "description": "Public player profile by username: ELO, record, playing style. Private fields are never included.",
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "description": "Player username or slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  }
}