{
  "openapi": "3.1.0",
  "info": {
    "title": "Codebattle Public API",
    "version": "1.0.0",
    "description": "Narrow, me-centric API for bots, CLIs and devices. Every request needs a personal token (Settings → API tokens): `Authorization: Bearer cbp_...`. Objects you are not related to answer 404. Limits are per user: 60 requests/min, 5 games/min and 30/day, 5 tournaments/day, at most 3 unfinished API-created tournaments. In v1 only additive changes happen. Human and LLM-friendly docs: https://codebattle.hexlet.io/api-docs (Markdown: /api-docs.md)."
  },
  "servers": [
    {
      "url": "https://codebattle.hexlet.io/public_api/v1"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Scopes: read, games:write (also tournament join/leave), tournaments:write"
      }
    },
    "schemas": {
      "Player": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "avatar_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "rating": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Place in the global rating"
          },
          "lang": {
            "type": [
              "string",
              "null"
            ],
            "description": "Preferred programming language"
          }
        },
        "example": {
          "id": 42,
          "name": "ada",
          "avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
          "rating": 1461,
          "rank": 12,
          "lang": "dlang"
        },
        "description": "A person: you (`GET /me`), an online user, a game player, an invite's creator or recipient. Those objects extend it."
      },
      "GamePlayer": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Player"
          },
          {
            "type": "object",
            "properties": {
              "lang": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Language of the player's editor in this game"
              },
              "is_bot": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "result": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "won",
                  "lost",
                  "gave_up",
                  "timeout",
                  null
                ],
                "description": "`null` until the player's game is decided"
              },
              "result_percent": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Hidden for opponents while the game is live"
              }
            }
          }
        ],
        "example": {
          "id": 42,
          "name": "ada",
          "avatar_url": null,
          "rating": 1461,
          "rank": 12,
          "lang": "python",
          "is_bot": false,
          "result": "won",
          "result_percent": 100.0
        }
      },
      "Game": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "example": "https://codebattle.hexlet.io/games/324274"
          },
          "state": {
            "type": "string",
            "enum": [
              "waiting_opponent",
              "playing",
              "game_over",
              "timeout",
              "canceled"
            ]
          },
          "level": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "timeout_seconds": {
            "type": "integer"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "starts_at + timeout_seconds: when the game times out"
          },
          "finishes_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "tournament_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "task": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Task"
              },
              {
                "type": "null"
              }
            ],
            "description": "`null` while the game waits for an opponent, unless it is your game"
          },
          "players": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GamePlayer"
            }
          }
        },
        "example": {
          "id": 324274,
          "url": "https://codebattle.hexlet.io/games/324274",
          "state": "game_over",
          "level": "easy",
          "type": "duo",
          "starts_at": "2026-10-10T18:40:00Z",
          "timeout_seconds": 600,
          "ends_at": "2026-10-10T18:50:00Z",
          "finishes_at": "2026-10-10T18:46:12Z",
          "tournament_id": null,
          "task": {
            "id": 1201,
            "name": "pair_with_maximum_product",
            "level": "easy",
            "tags": [
              "collections"
            ],
            "visibility": "public",
            "url": "https://codebattle.hexlet.io/tasks/1201"
          },
          "players": [
            {
              "id": 42,
              "name": "ada",
              "avatar_url": null,
              "rating": 1461,
              "rank": 12,
              "lang": "python",
              "is_bot": false,
              "result": "won",
              "result_percent": 100.0
            },
            {
              "id": -1,
              "name": "Codebot",
              "avatar_url": null,
              "rating": 1200,
              "rank": null,
              "lang": "js",
              "is_bot": true,
              "result": "lost",
              "result_percent": 40.0
            }
          ]
        }
      },
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Unique, e.g. `minimal_time_partition`; usable as `task_name`"
          },
          "level": {
            "type": "string",
            "enum": [
              "elementary",
              "easy",
              "medium",
              "hard"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "hidden"
            ],
            "description": "`hidden` tasks are listed only for their creator"
          },
          "url": {
            "type": "string"
          }
        },
        "example": {
          "id": 42,
          "name": "minimal_time_partition",
          "level": "medium",
          "tags": [
            "math",
            "collections"
          ],
          "visibility": "public",
          "url": "https://codebattle.hexlet.io/tasks/42"
        }
      },
      "Tournament": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "upcoming",
              "waiting_participants",
              "active",
              "finished",
              "canceled",
              "timeout"
            ]
          },
          "level": {
            "type": "string"
          },
          "grade": {
            "type": "string",
            "description": "`open` for user tournaments; season grades for official ones"
          },
          "access_type": {
            "type": "string",
            "enum": [
              "public",
              "token"
            ]
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "players_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "players_limit": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rounds_limit": {
            "type": [
              "integer",
              "null"
            ]
          },
          "current_round": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "position": {
                "type": "integer"
              },
              "started_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "break": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean"
              },
              "ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "joined": {
            "type": "boolean",
            "description": "Whether you are a player of this tournament"
          },
          "access_token": {
            "type": "string",
            "description": "Creator and moderators only"
          },
          "invite_url": {
            "type": "string",
            "description": "Creator and moderators only"
          }
        },
        "example": {
          "id": 5775,
          "name": "Friday practice",
          "url": "https://codebattle.hexlet.io/tournaments/5775",
          "type": "swiss",
          "state": "active",
          "level": "easy",
          "grade": "open",
          "access_type": "token",
          "starts_at": "2026-10-10T19:00:00Z",
          "started_at": "2026-10-10T19:00:04Z",
          "players_count": 8,
          "players_limit": 16,
          "rounds_limit": 3,
          "current_round": {
            "position": 1,
            "started_at": "2026-10-10T19:12:00Z",
            "ends_at": "2026-10-10T19:22:00Z"
          },
          "break": {
            "active": false,
            "ends_at": null
          },
          "access_token": "Xy7pQw2LmN4rTz8aB",
          "invite_url": "https://codebattle.hexlet.io/tournaments/5775?access_token=Xy7pQw2LmN4rTz8aB"
        }
      },
      "RankingEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "place": {
            "type": "integer"
          },
          "score": {
            "type": "number"
          },
          "lang": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "example": {
          "id": 42,
          "name": "ada",
          "place": 1,
          "score": 42,
          "lang": "python"
        }
      },
      "TournamentInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 3,
            "maxLength": 100,
            "example": "Friday practice"
          },
          "description": {
            "type": "string",
            "minLength": 3,
            "maxLength": 2000,
            "example": "Weekly practice for our team"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time",
            "description": "In the future, at most 30 days ahead",
            "example": "2026-10-17T19:00:00Z"
          },
          "level": {
            "type": "string",
            "enum": [
              "elementary",
              "easy",
              "medium",
              "hard"
            ],
            "example": "easy"
          },
          "players_limit": {
            "type": "integer",
            "minimum": 2,
            "maximum": 64,
            "example": 16
          },
          "rounds_limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "example": 3
          },
          "timeout_mode": {
            "type": "string",
            "enum": [
              "per_task",
              "per_round_fixed"
            ]
          },
          "match_timeout_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 3600
          },
          "round_timeout_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 3600
          },
          "break_duration_seconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 600
          },
          "use_chat": {
            "type": "boolean"
          }
        }
      },
      "Invite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "canceled",
              "expired",
              "dropped"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ],
            "description": "`incoming`: you are the recipient and can accept"
          },
          "level": {
            "type": [
              "string",
              "null"
            ]
          },
          "timeout_seconds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "creator": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Player"
              },
              {
                "type": "null"
              }
            ]
          },
          "recipient": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Player"
              },
              {
                "type": "null"
              }
            ]
          },
          "task_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Task chosen by the creator, if any"
          },
          "game_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Set once accepted"
          },
          "game_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "example": {
          "id": 9001,
          "state": "pending",
          "direction": "incoming",
          "level": "easy",
          "timeout_seconds": 600,
          "creator": {
            "id": 77,
            "name": "grace",
            "avatar_url": null,
            "rating": 1520
          },
          "recipient": {
            "id": 42,
            "name": "ada",
            "avatar_url": null,
            "rating": 1461
          },
          "task_id": null,
          "game_id": null,
          "game_url": null,
          "created_at": "2026-10-10T19:01:00Z"
        }
      },
      "OnlineUser": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Player"
          },
          {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "online",
                  "lobby",
                  "task",
                  "tournament",
                  "watching",
                  "playing"
                ],
                "description": "The busiest of the user's open tabs"
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Game or tournament the user is on, if any"
              }
            }
          }
        ],
        "example": {
          "id": 77,
          "name": "grace",
          "avatar_url": null,
          "rating": 1520,
          "rank": 8,
          "lang": "dlang",
          "state": "playing",
          "url": "https://codebattle.hexlet.io/games/324274"
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "invalid_token",
                  "insufficient_scope",
                  "not_found",
                  "unsupported_media_type",
                  "validation_failed",
                  "quota_exceeded",
                  "rate_limited",
                  "api_disabled"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object"
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "not_found",
            "message": "Not found"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The token lacks the scope",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found or not related to you",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit or quota exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "Invalid parameters; unknown fields are rejected",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "The body is not sent as application/json",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/me": {
      "get": {
        "summary": "Current user",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/Player"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "description": "Who the token belongs to. A cheap way to check that a token works."
      }
    },
    "/me/active_game": {
      "get": {
        "summary": "Current live game (null if none). Count down with ends_at - server_time.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "game": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Game"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "game": {
                    "id": 324274,
                    "url": "https://codebattle.hexlet.io/games/324274",
                    "state": "playing",
                    "level": "easy",
                    "type": "duo",
                    "starts_at": "2026-10-10T18:40:00Z",
                    "timeout_seconds": 600,
                    "ends_at": "2026-10-10T18:50:00Z",
                    "finishes_at": null,
                    "tournament_id": null,
                    "task": {
                      "id": 1201,
                      "name": "pair_with_maximum_product",
                      "level": "easy",
                      "tags": [
                        "collections"
                      ],
                      "visibility": "public",
                      "url": "https://codebattle.hexlet.io/tasks/1201"
                    },
                    "players": [
                      {
                        "id": 42,
                        "name": "ada",
                        "avatar_url": null,
                        "rating": 1461,
                        "rank": 12,
                        "lang": "python",
                        "is_bot": false,
                        "result": null,
                        "result_percent": 60.0
                      },
                      {
                        "id": -1,
                        "name": "Codebot",
                        "avatar_url": null,
                        "rating": 1200,
                        "rank": null,
                        "lang": "js",
                        "is_bot": true,
                        "result": null
                      }
                    ]
                  },
                  "server_time": "2026-10-10T18:43:20Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "description": "Your live game, or `null`: the one you are playing, or one you created that still waits for an opponent (`state: waiting_opponent`). Poll it (every few seconds is fine) to notice a game started from the web or by a tournament. Compute the remaining time as `ends_at - server_time`, not against your own clock."
      }
    },
    "/me/games": {
      "get": {
        "summary": "My finished games, newest first",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Game"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "description": "`next_cursor` from the previous page: returns games with a smaller id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only games started at or after this moment. ISO 8601 date (`2026-10-01`, midnight UTC) or datetime.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-10-01"
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only games started before this moment. ISO 8601 date or datetime.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-10-10T18:00:00Z"
          }
        ],
        "description": "Finished games (`game_over` or `timeout`), 50 per page, newest first. Pagination is keyset-based: `next_cursor` is the id of the last game on the page; pass it back as `cursor` to get the next, older page. `next_cursor` is `null` on the last page. `from`/`to` narrow the result by game start time and combine with `cursor`."
      }
    },
    "/me/tournaments": {
      "get": {
        "summary": "Tournaments I created, moderate or play",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournaments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tournament"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by state, e.g. `active` or `finished`."
          }
        ],
        "description": "Up to 50 tournaments you created, moderate or joined as a player, newest first."
      }
    },
    "/me/invites": {
      "get": {
        "summary": "My pending invites, incoming and outgoing",
        "description": "Pending invites where you are the creator or the recipient. Supports long polling instead of frequent polls: pass `wait` (seconds, up to 30) and the `version` from your previous response. If the list already differs from that version the answer is immediate; otherwise the request is held until an invite is created, accepted or canceled, or until `wait` runs out, and then returns the current list. Loop on it to react to invites within a second with about two requests a minute. An accepted invite leaves the list; the creator finds the game via `GET /me/active_game`.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "wait",
            "in": "query",
            "description": "Hold the request up to this many seconds (0..30) while nothing changes.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 30
            },
            "example": 25
          },
          {
            "name": "version",
            "in": "query",
            "description": "`version` from the previous response.",
            "schema": {
              "type": "string"
            },
            "example": "1x3kq9"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invite"
                      }
                    },
                    "version": {
                      "type": "string",
                      "description": "Changes whenever the list changes; pass it back with `wait`"
                    },
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "invites": [
                    {
                      "id": 9001,
                      "state": "pending",
                      "direction": "incoming",
                      "level": "easy",
                      "timeout_seconds": 600,
                      "creator": {
                        "id": 77,
                        "name": "grace",
                        "avatar_url": null,
                        "rating": 1520
                      },
                      "recipient": {
                        "id": 42,
                        "name": "ada",
                        "avatar_url": null,
                        "rating": 1461
                      },
                      "game_id": null,
                      "game_url": null,
                      "created_at": "2026-10-10T19:01:00Z"
                    }
                  ],
                  "version": "1x3kq9",
                  "server_time": "2026-10-10T19:01:05Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/me/events": {
      "get": {
        "summary": "Live stream of everything that happens to you (SSE)",
        "description": "A Server-Sent Events stream: one request that stays open. It starts with `hello` (your current state: active game, pending invites, unfinished tournaments), then sends one event per change: invites, your games, tournaments you play in or run. A `: ping` comment comes every 15 seconds. The server closes the stream after an hour with a `bye` event; reconnect and you get a fresh `hello`. At most 2 open streams per user. See **Event stream** above for every event and client examples.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "An open event stream; see the Event stream section for every event",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "retry: 3000\n\nid: 1791702000000\nevent: hello\ndata: {\"user\":{\"id\":42,\"name\":\"ada\",...},\"active_game\":null,\"invites\":[],\"tournaments\":[],\"server_time\":\"2026-10-11T07:00:00Z\"}\n\nid: 1791702012345\nevent: invite.created\ndata: {\"invite\":{\"id\":9001,\"direction\":\"incoming\",\"creator\":{\"id\":77,\"name\":\"grace\",...},...},\"server_time\":\"2026-10-11T07:00:12Z\"}\n\n: ping\n\nid: 1791702020000\nevent: game.started\ndata: {\"game\":{\"id\":324274,\"state\":\"playing\",\"url\":\"https://codebattle.hexlet.io/games/324274\",...},\"server_time\":\"2026-10-11T07:00:20Z\"}"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tournaments/schedule": {
      "get": {
        "summary": "Public upcoming and active tournaments for the next 14 days",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournaments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tournament"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "description": "Public tournaments that start between 1 day ago and 14 days ahead and are not finished yet: what the lobby shows as upcoming. For everything running right now use `GET /tournaments/live`."
      }
    },
    "/tournaments/live": {
      "get": {
        "summary": "Tournaments running now or open for joining",
        "description": "Currently running tournament processes in `waiting_participants` or `active` state: public ones plus private ones you created, moderate or play in. Uses live state and participant counts, excluding abandoned database records. A running tournament can have an old `starts_at`. Newest first, at most 100.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournaments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tournament"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tournaments": {
      "post": {
        "summary": "Create a private tournament (always grade open, tasks by level)",
        "security": [
          {
            "bearerAuth": [
              "tournaments:write"
            ]
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 2000
                  },
                  "starts_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "In the future, at most 30 days ahead"
                  },
                  "level": {
                    "type": "string",
                    "enum": [
                      "elementary",
                      "easy",
                      "medium",
                      "hard"
                    ]
                  },
                  "players_limit": {
                    "type": "integer",
                    "minimum": 2,
                    "maximum": 64
                  },
                  "rounds_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10
                  },
                  "timeout_mode": {
                    "type": "string",
                    "enum": [
                      "per_task",
                      "per_round_fixed"
                    ]
                  },
                  "match_timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600
                  },
                  "round_timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600
                  },
                  "break_duration_seconds": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 600
                  },
                  "use_chat": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "name",
                  "description",
                  "starts_at",
                  "level"
                ]
              }
            }
          }
        },
        "description": "Creates a private (`access_type: token`) tournament of grade `open`; tasks are picked by `level`. Share `invite_url` with players. Quota: 5 per day and at most 3 unfinished API-created tournaments."
      }
    },
    "/tournaments/{id}": {
      "get": {
        "summary": "Tournament card: related to you, or public and scheduled",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "description": "Works for tournaments related to you and for public ones. The creator and moderators also get `access_token` and `invite_url`."
      },
      "patch": {
        "summary": "Edit your tournament before it starts",
        "security": [
          {
            "bearerAuth": [
              "tournaments:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 2000
                  },
                  "starts_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "In the future, at most 30 days ahead"
                  },
                  "level": {
                    "type": "string",
                    "enum": [
                      "elementary",
                      "easy",
                      "medium",
                      "hard"
                    ]
                  },
                  "players_limit": {
                    "type": "integer",
                    "minimum": 2,
                    "maximum": 64
                  },
                  "rounds_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10
                  },
                  "timeout_mode": {
                    "type": "string",
                    "enum": [
                      "per_task",
                      "per_round_fixed"
                    ]
                  },
                  "match_timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600
                  },
                  "round_timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600
                  },
                  "break_duration_seconds": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 600
                  },
                  "use_chat": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "description": "Only the fields you send change. Allowed while the tournament is `upcoming` or `waiting_participants`."
      }
    },
    "/tournaments/{id}/ranking": {
      "get": {
        "summary": "Ranking: full and paginated for the creator/moderators, top 20 plus your row for participants",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ranking": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RankingEntry"
                      }
                    },
                    "me": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/RankingEntry"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Participants only: your own row"
                    },
                    "page_info": {
                      "type": "object",
                      "description": "Creator and moderators only",
                      "properties": {
                        "page_number": {
                          "type": "integer"
                        },
                        "page_size": {
                          "type": "integer"
                        },
                        "total_entries": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 100
            }
          }
        ],
        "description": "The creator and moderators get the full ranking with `page` / `page_size` (max 100). Participants get the top 20 and their own row in `me`."
      }
    },
    "/tournaments/{id}/start": {
      "post": {
        "summary": "Start your tournament",
        "security": [
          {
            "bearerAuth": [
              "tournaments:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "description": "Starts your tournament now with the players who have joined."
      }
    },
    "/tournaments/{id}/cancel": {
      "post": {
        "summary": "Cancel your tournament",
        "security": [
          {
            "bearerAuth": [
              "tournaments:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "description": "Cancels your tournament and finishes its games."
      }
    },
    "/tournaments/{id}/join": {
      "post": {
        "summary": "Join a tournament as a player",
        "description": "Same as the Join button on the tournament page: works for a public tournament from the schedule, or a private one when you pass the `access_token` from its `invite_url` (without it, a private tournament you are not related to answers 404). Possible while the tournament is `waiting_participants`, and for swiss and ladder also while it is `active`. Joining twice is not an error: you get the card with `joined: true`. Then wait for `tournament.started` / `game.started` on `GET /me/events` and open the game `url` only when it begins, instead of keeping the tournament page open. A failed join answers 422 `validation_failed` with `details.reason`: `not_open` (finished, canceled or not open yet), `full` (`players_limit` reached), `not_running` (the tournament process is stopped; an organizer has to restart it) or `rejected` (the tournament didn't accept the join).",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "access_token": {
                    "type": "string",
                    "description": "The `access_token` query parameter of a private tournament's `invite_url`"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined (or already a player)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tournaments/{id}/leave": {
      "post": {
        "summary": "Leave a tournament before it starts",
        "description": "Removes you from the players while the tournament is `waiting_participants`. Leaving a tournament you don't play in returns the card with `joined: false`. After the start it answers 422 `validation_failed` with `details.reason` `already_started`; `not_running` means the tournament process is stopped.",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tournament": {
                      "$ref": "#/components/schemas/Tournament"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks": {
      "get": {
        "summary": "Tasks you can play",
        "description": "Public active tasks plus your own, ordered by level and name. Use an `id` or `name` from here in `POST /games` or `POST /invites`.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "description": "Only tasks of this level.",
            "schema": {
              "type": "string",
              "enum": [
                "elementary",
                "easy",
                "medium",
                "hard"
              ]
            },
            "example": "medium"
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated; tasks having any of these tags.",
            "schema": {
              "type": "string"
            },
            "example": "strings,math"
          },
          {
            "name": "name",
            "in": "query",
            "description": "Case-insensitive substring of the task name.",
            "schema": {
              "type": "string"
            },
            "example": "partition"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tasks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/games/live": {
      "get": {
        "summary": "Live games",
        "description": "What the lobby shows: games being played and games waiting for an opponent (usually a few Codebot games are waiting), newest first. Tournament games and other people's link games are not listed. Pass `state=waiting_opponent` for just the games you can join, then join one with `POST /games/{id}/join`; their task stays hidden until someone joins. Changes often; poll every 10–30 seconds.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "Only games in this state: `waiting_opponent` (open to join) or `playing`.",
            "schema": {
              "type": "string",
              "enum": [
                "waiting_opponent",
                "playing"
              ]
            },
            "example": "waiting_opponent"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Game"
                      }
                    },
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/games": {
      "post": {
        "summary": "Create a game: against a bot, or waiting for an opponent who joins by link",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "game": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Game"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "join_url": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "For `opponent: link`: the page to send your opponent"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "level": {
                    "type": "string",
                    "enum": [
                      "elementary",
                      "easy",
                      "medium",
                      "hard"
                    ],
                    "description": "Required unless `task_id` or `task_name` is given; with a task it must equal the task's level."
                  },
                  "task_id": {
                    "type": "integer",
                    "description": "Play this task (from `GET /tasks`). Pass this or `task_name`; without both a random task of `level` is picked.",
                    "example": 42
                  },
                  "task_name": {
                    "type": "string",
                    "description": "Exact task name, e.g. `minimal_time_partition`"
                  },
                  "opponent": {
                    "type": "string",
                    "enum": [
                      "bot",
                      "link"
                    ]
                  },
                  "timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600,
                    "default": 600
                  }
                },
                "required": [
                  "opponent"
                ]
              }
            }
          }
        },
        "description": "`opponent: bot` starts the game immediately against Codebot. `opponent: link` creates a hidden game waiting for an opponent; send them `join_url`. Pick the task with `task_id` or `task_name` (see `GET /tasks`), or let the server pick a random one of `level`. Only tasks you may play are accepted: public ones and your own; any other answers 422 `no such task`. You can have only one active game at a time. Quota: 5 games per minute, 30 per day."
      }
    },
    "/games/{id}/join": {
      "post": {
        "summary": "Join a game that waits for an opponent",
        "description": "Joins a game from `GET /games/live?state=waiting_opponent`, or a `link` game whose id someone shared with you. The game starts at once; open its `url`. A failed join answers 422 `validation_failed` with `details.reason`: `not_waiting_opponent` (the game already started), `already_in_a_game` (you play another game: `details.active_game_id`; finish it or `POST /games/{id}/cancel` it if it still waits) or `already_joined` (you are already a player of this game).",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Joined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "game": {
                      "$ref": "#/components/schemas/Game"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/games/{id}/cancel": {
      "post": {
        "summary": "Cancel your game that waits for an opponent",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "game": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "canceled"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "description": "Only a game in `waiting_opponent` state can be canceled."
      }
    },
    "/invites": {
      "post": {
        "summary": "Invite a user to a game",
        "description": "Sends an invite that the recipient sees in the lobby and in `GET /me/invites`. One pending invite per pair of users.",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "recipient_id": {
                    "type": "integer",
                    "description": "Whom to invite. Pass this or `recipient_name`.",
                    "example": 77
                  },
                  "recipient_name": {
                    "type": "string",
                    "description": "Exact user name, handy for voice commands"
                  },
                  "level": {
                    "type": "string",
                    "enum": [
                      "elementary",
                      "easy",
                      "medium",
                      "hard"
                    ],
                    "example": "easy"
                  },
                  "task_id": {
                    "type": "integer",
                    "description": "Play this task (from `GET /tasks`). Pass this or `task_name`; without both a random task of `level` is picked.",
                    "example": 42
                  },
                  "task_name": {
                    "type": "string",
                    "description": "Exact task name, e.g. `minimal_time_partition`"
                  },
                  "timeout_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 3600,
                    "description": "Game length, default 3600"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/invites/{id}/accept": {
      "post": {
        "summary": "Accept an invite sent to you",
        "description": "Starts the game for both players; the response has `game_id` and `game_url`. Fails with `422` if one of you is already in a game (the invite is then dropped).",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/invites/{id}/cancel": {
      "post": {
        "summary": "Decline or withdraw an invite",
        "description": "The recipient declines, the creator withdraws. Only pending invites.",
        "security": [
          {
            "bearerAuth": [
              "games:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          }
        }
      }
    },
    "/users/online": {
      "get": {
        "summary": "Who is online",
        "description": "Registered users with codebattle open right now, highest rating first, with what they are doing. Changes slowly: polling every 30–60 seconds is plenty.",
        "security": [
          {
            "bearerAuth": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OnlineUser"
                      }
                    },
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  }
}
