{
  "openapi": "3.1.1",
  "info": {
    "title": "Team Wheel API",
    "description": "A no-login team wheel for picking who goes next. Create a room, add members, start a wheel, then spin, accept the pick or put a name back. The room id is the only access control: anyone with it can change the room, so treat it like a password and never publish it. Writes use optimistic concurrency: send the room's version (from its ETag or the version field) in If-Match. A 412 response carries the current room, so read its version and retry. People open a room at https://wheel.vibeworld.shop/r/{id}.",
    "version": "1"
  },
  "servers": [
    {
      "url": "https://wheel.vibeworld.shop"
    }
  ],
  "paths": {
    "/api/rooms": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "summary": "Create a room",
        "description": "The name is optional. Returns the new room; share https://wheel.vibeworld.shop/r/{id} with the team.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/CreateRoomRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "summary": "Get a room",
        "description": "Members, the current wheel and the version to send in If-Match on the next write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Rooms"
        ],
        "summary": "Rename a room",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NameRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Rooms"
        ],
        "summary": "Delete a room",
        "description": "confirm must be the room's name, or the word delete if it has none. This cannot be undone.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteRoomRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/history": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "summary": "Get a room's history",
        "description": "Newest first. winnersOnly keeps only accepted picks; before pages back from an event id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "winnersOnly",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventView"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/members": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Add a member",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NameRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/members/{memberId}": {
      "delete": {
        "tags": [
          "Members"
        ],
        "summary": "Remove a member",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/wheels": {
      "post": {
        "tags": [
          "Wheel"
        ],
        "summary": "Start a wheel",
        "description": "names lists who goes on the wheel: at least 2, each already a member of the room (add them with POST /api/rooms/{id}/members first). Starts a fresh wheel, replacing the current one.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartWheelRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/wheel/spin": {
      "post": {
        "tags": [
          "Wheel"
        ],
        "summary": "Spin the wheel",
        "description": "The server picks a name at random from those not yet picked and shows it as pending. Accept it to take it off the wheel, or spin again to re-draw.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/wheel/accept": {
      "post": {
        "tags": [
          "Wheel"
        ],
        "summary": "Accept the pending pick",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/rooms/{id}/wheel/put-back": {
      "post": {
        "tags": [
          "Wheel"
        ],
        "summary": "Put a picked name back on the wheel",
        "description": "Undoes an accepted pick so that name can be drawn again. Reopens a finished wheel.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The room version you last saw, for example \"7\".",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NameRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomView"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateRoomRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "DeleteRoomRequest": {
        "required": [
          "confirm"
        ],
        "type": "object",
        "properties": {
          "confirm": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "ErrorResponse": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "EventView": {
        "required": [
          "id",
          "kind",
          "name",
          "detail",
          "at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": [
              "null",
              "string"
            ]
          },
          "detail": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/JsonElement"
              }
            ]
          },
          "at": {
            "type": "string"
          }
        }
      },
      "JsonElement": {},
      "MemberView": {
        "required": [
          "id",
          "name"
        ],
        "type": "object",
        "properties": {
          "id": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "NameRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "RoomView": {
        "required": [
          "id",
          "name",
          "version",
          "lastActivityAt",
          "members",
          "wheel"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "null",
              "string"
            ]
          },
          "version": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "lastActivityAt": {
            "type": "string"
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MemberView"
            }
          },
          "wheel": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/WheelView"
              }
            ]
          }
        }
      },
      "StartWheelRequest": {
        "required": [
          "names"
        ],
        "type": "object",
        "properties": {
          "names": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WheelView": {
        "required": [
          "startedAt",
          "finished",
          "pending",
          "remaining",
          "picked"
        ],
        "type": "object",
        "properties": {
          "startedAt": {
            "type": "string"
          },
          "finished": {
            "type": "boolean"
          },
          "pending": {
            "type": [
              "null",
              "string"
            ]
          },
          "remaining": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "picked": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Rooms"
    },
    {
      "name": "Members"
    },
    {
      "name": "Wheel"
    }
  ]
}
