{
  "openapi": "3.1.0",
  "info": {
    "title": "BJJGym public discovery API",
    "version": "1.0.0",
    "description": "BJJGym is a Brazilian Jiu-Jitsu gym directory. The GET endpoints below are public. No API key, account, or authentication header is required. Use a city name and country code together to find gyms. City search returns at most 10 results. Gym search returns at most 50 results. There is no pagination or full export. Missing facts are unknown. Confirm current times and prices with the gym. Full Ask answers require a member account. Private admin and account routes are outside this public contract."
  },
  "servers": [{"url": "https://bjjgym.com"}],
  "paths": {
    "/api/cities": {
      "get": {
        "operationId": "searchCities",
        "summary": "Find cities",
        "security": [],
        "description": "No authentication required. Example: curl -sS 'https://bjjgym.com/api/cities?search=Barcelona'",
        "parameters": [{"name": "search", "in": "query", "description": "Part of a city name.", "schema": {"type": "string"}}],
        "responses": {
          "200": {"description": "Up to ten matching cities.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/City"}}}}},
          "500": {"$ref": "#/components/responses/ApiError"}
        }
      }
    },
    "/api/gyms": {
      "get": {
        "operationId": "searchGyms",
        "summary": "Find gyms",
        "security": [],
        "description": "No authentication required. Example: curl -sS 'https://bjjgym.com/api/gyms?city=Barcelona&country=ES'",
        "parameters": [
          {"name": "search", "in": "query", "description": "Part of a gym, city, or country name.", "schema": {"type": "string"}},
          {"name": "city", "in": "query", "description": "City name.", "schema": {"type": "string"}},
          {"name": "country", "in": "query", "description": "Two-letter country code.", "schema": {"type": "string"}},
          {"name": "userId", "in": "query", "description": "Owner user ID.", "schema": {"type": "string"}}
        ],
        "responses": {
          "200": {"description": "Up to 50 gyms.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Gym"}}}}},
          "500": {"$ref": "#/components/responses/ApiError"}
        }
      }
    },
    "/api/gym/{id}": {
      "get": {
        "operationId": "getGym",
        "summary": "Get a gym by ID",
        "security": [],
        "description": "No authentication required. Replace GYM_ID with an id from gym search. Example: curl -sS 'https://bjjgym.com/api/gym/GYM_ID'",
        "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}],
        "responses": {
          "200": {"description": "The gym record.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Gym"}}}},
          "404": {"$ref": "#/components/responses/ApiError"},
          "500": {"$ref": "#/components/responses/ApiError"}
        }
      }
    },
    "/api/ask": {
      "post": {
        "operationId": "askForGyms",
        "summary": "Find gyms from a training question",
        "description": "Signed-out visitors get limited answers. Members get full answers. The service applies a rate limit.",
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string", "minLength": 1, "maxLength": 300}}}}}},
        "responses": {
          "200": {
            "description": "A result state, evidence coverage, and any confirmed gyms.",
            "content": {"application/json": {"schema": {
              "type": "object",
              "required": ["status", "locked", "location", "coverage", "results", "total"],
              "additionalProperties": true,
              "properties": {
                "status": {"type": "string", "enum": ["results", "partial", "needs_location", "unsupported", "insufficient_data", "no_known_match", "out_of_scope"]},
                "message": {"type": ["string", "null"]},
                "action": {"type": ["object", "null"], "properties": {"label": {"type": "string"}, "href": {"type": "string"}}},
                "location": {"type": "object", "required": ["kind", "name", "href"], "properties": {"kind": {"type": "string", "enum": ["city", "country", "missing", "unresolved"]}, "name": {"type": ["string", "null"]}, "href": {"type": "string"}}},
                "coverage": {"type": "object", "required": ["eligible", "full", "conditions"], "properties": {"eligible": {"type": "integer"}, "full": {"type": "integer"}, "conditions": {"type": "array", "items": {"type": "object", "required": ["key", "label", "known", "matched"], "properties": {"key": {"type": "string"}, "label": {"type": "string"}, "known": {"type": "integer"}, "matched": {"type": "integer"}}}}}},
                "locked": {"type": "boolean"},
                "city": {"type": ["string", "null"]},
                "cityHref": {"type": ["string", "null"]},
                "outOfScope": {"type": "boolean"},
                "results": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true,
                    "properties": {
                      "evidence": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": ["key", "label", "state"],
                          "properties": {
                            "key": {"type": "string"},
                            "label": {"type": "string"},
                            "state": {"type": "string", "enum": ["confirmed", "not_met", "unknown"]}
                          }
                        }
                      }
                    }
                  }
                },
                "top": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
                "total": {"type": "integer"}
              }
            }}}
          },
          "400": {"$ref": "#/components/responses/ApiError"},
          "429": {"$ref": "#/components/responses/ApiError"},
          "503": {"$ref": "#/components/responses/ApiError"}
        }
      }
    }
  },
  "components": {
    "schemas": {
      "City": {"type": "object", "required": ["id", "city", "country", "iso2"], "properties": {"id": {"type": "string"}, "city": {"type": "string"}, "country": {"type": "string"}, "iso2": {"type": "string"}}},
      "Gym": {"type": "object", "additionalProperties": true, "properties": {"id": {"type": "string"}, "storeName": {"type": "string"}}},
      "Error": {"type": "object", "required": ["code", "message", "hint"], "additionalProperties": true, "properties": {"code": {"type": "string"}, "message": {"type": "string"}, "hint": {"type": "string"}}}
    },
    "responses": {
      "ApiError": {"description": "A structured error with a code and a recovery hint.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    }
  }
}
