{
  "openapi": "3.1.0",
  "info": {
    "title": "Phroller Companion API",
    "version": "1",
    "description": "A local REST, WebSocket and MCP API that lets other programs on the same computer read the open map and build on it live: stream overlays, macro pads, map generators, scripts and agents.\n\n- **Desktop only**: macOS and Windows. Off by default; switched on in **Settings › System › Companion API**, which also shows the key.\n- **Loopback only**: `127.0.0.1`, port `47321` unless changed there. Every request carries `Authorization: Bearer <key>`; a `GET` may pass `?token=<key>` instead.\n- **Every write is an ordinary edit**: seen at once, sent to everyone at a shared table, and one step of undo (`undo_last` takes back the API's own).\n- **Coordinates**: a token stands in cell `[x, y]` (one cell is 60 board pixels); walls, doors and rectangles run along points, point `[x, y]` being the top-left corner of cell `[x, y]`. Facing is a compass bearing: 0 north, 90 east.\n- **Answers**: `{\"apiVersion\": \"1\", \"ok\": true, \"result\": {…}}`, or `ok: false` with an `error`. A key a command did not use is named in `result.warnings` (or `error.warnings`), with the field it probably meant. A picture comes back base64 in `png`.\n- **Pictures**: `screenshot` uses a camera of the caller's own, separate from the player's — moving one never moves the other — and every camera answer says where it points.\n- **Questions about the app**: `get_help` answers from the app's own Help.\n- **Versioning**: commands and fields may be added within `/v1`; renaming or removing one needs `/v2`.",
    "contact": {
      "name": "Phroller",
      "url": "https://phroller.com"
    }
  },
  "servers": [
    {
      "url": "http://127.0.0.1:47321",
      "description": "This computer, on the default port"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    },
    {
      "TokenQuery": []
    }
  ],
  "tags": [
    {
      "name": "System",
      "description": "What the server offers, uploads, and the event stream."
    },
    {
      "name": "Guides & Help",
      "description": "The reference guides and the app's own Help, as text."
    },
    {
      "name": "Board",
      "description": "Read the open map, check space on it, and switch, open or undo maps."
    },
    {
      "name": "Camera & Screenshots",
      "description": "Pictures of the board from a camera of the caller's own, and the player's."
    },
    {
      "name": "Assets",
      "description": "Find, upload and import art."
    },
    {
      "name": "Building",
      "description": "Tokens, props, decals, walls, doors, levels, fog and the environment."
    },
    {
      "name": "Playing",
      "description": "What every player does at the table."
    },
    {
      "name": "Dice",
      "description": "Dice on the board, rolls, and the dice tabs."
    },
    {
      "name": "Widgets",
      "description": "A dice tab's or a token's widgets."
    },
    {
      "name": "Library Tokens",
      "description": "Tokens and props in the library."
    },
    {
      "name": "Cards",
      "description": "Decks, piles and hands on the table."
    },
    {
      "name": "Card Designs",
      "description": "Card designs in Your Sets."
    },
    {
      "name": "Smart Objects",
      "description": "Packages, and the smart objects placed from them."
    },
    {
      "name": "MCP",
      "description": "Every command as an MCP tool, over streamable HTTP."
    }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "server_info",
        "tags": [
          "System"
        ],
        "summary": "What the server offers",
        "description": "The app's version and where the MCP endpoint, the command list and the event stream are.",
        "responses": {
          "200": {
            "description": "The server.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": {
                      "type": "string",
                      "const": "1"
                    },
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "result": {
                      "type": "object",
                      "properties": {
                        "app": {
                          "type": "string",
                          "example": "Phroller"
                        },
                        "version": {
                          "type": "string"
                        },
                        "mcp": {
                          "type": "string",
                          "const": "/mcp"
                        },
                        "events": {
                          "type": "string",
                          "const": "/v1/events"
                        },
                        "commands": {
                          "type": "string",
                          "const": "/v1/commands"
                        }
                      }
                    }
                  },
                  "required": [
                    "apiVersion",
                    "ok",
                    "result"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/commands": {
      "get": {
        "operationId": "list_commands",
        "tags": [
          "System"
        ],
        "summary": "Every command and its schema",
        "description": "The list MCP `tools/list` is made from: each command's name, description, input schema and whether it writes.",
        "responses": {
          "200": {
            "description": "The commands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": {
                      "type": "string",
                      "const": "1"
                    },
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "result": {
                      "type": "object",
                      "properties": {
                        "commands": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              },
                              "inputSchema": {
                                "type": "object"
                              },
                              "writes": {
                                "type": "boolean"
                              }
                            }
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "apiVersion",
                    "ok",
                    "result"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/board": {
      "get": {
        "operationId": "get_board_get",
        "tags": [
          "Board"
        ],
        "summary": "get_board, as a GET",
        "description": "`get_board` for a client that can only make a GET (a stream overlay's browser source). Summarise the open map: grid, levels, overall bounds in points/cells, object summary counts, and per level every token (with size, facing, elevation), prop, decal, wall (with bounds), door and die (with its face value, storey, colour and material) with its id and position in grid cells.",
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "required": false,
            "description": "Only this level, by id or name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The map.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/screenshot": {
      "post": {
        "operationId": "screenshot",
        "tags": [
          "Camera & Screenshots"
        ],
        "summary": "screenshot",
        "description": "A PNG from your own camera, apart from the player's. Aim it: centre, zoom, look (a compass direction), tilt, or frame a region or object; it keeps its aim and each answer says where it points. Cannot see something? Move it and try again before saying so or asking the player. target user: the player's screen.\n\nReads; changes nothing on the map. Answers with a picture (`png`, base64), and is refused when pictures are off in Settings.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": true
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "maxSize": {
                    "type": "integer",
                    "description": "256–2048. Default 1024."
                  },
                  "center": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells. Cell to centre on."
                  },
                  "zoom": {
                    "type": "number",
                    "description": "0.2–5."
                  },
                  "looking": {
                    "type": "string",
                    "enum": [
                      "north",
                      "north-east",
                      "east",
                      "south-east",
                      "south",
                      "south-west",
                      "west",
                      "north-west"
                    ],
                    "description": "Direction at the top of the picture."
                  },
                  "yaw": {
                    "type": "number",
                    "description": "Degrees; looking is easier."
                  },
                  "pitch": {
                    "type": "number",
                    "description": "0 is straight down, to 85; 45–75 shows heights and sides."
                  },
                  "region": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4,
                    "description": "[x, y, width, height]. Points [x, y, w, h] to frame."
                  },
                  "object": {
                    "type": "string",
                    "description": "An id from get_board or get_cards, framed close up."
                  },
                  "target": {
                    "type": "string",
                    "enum": [
                      "headless",
                      "user"
                    ],
                    "description": "Default headless, your camera; user is the player's screen as it is."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/upload": {
      "post": {
        "operationId": "upload",
        "tags": [
          "System"
        ],
        "summary": "Upload a file",
        "description": "Hands the app a file's bytes, for `import_asset` to import by `uploadId`. The way in on the App Store build, which cannot read a path it did not open itself. Up to 256 MB.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "The file name, with its extension: `goblin.png`, `tavern.glb`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": {
                      "type": "string",
                      "const": "1"
                    },
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "result": {
                      "type": "object",
                      "properties": {
                        "uploadId": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "bytes": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "uploadId",
                        "name",
                        "bytes"
                      ]
                    }
                  },
                  "required": [
                    "apiVersion",
                    "ok",
                    "result"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "events",
        "tags": [
          "System"
        ],
        "summary": "The event stream (WebSocket)",
        "description": "Connect a WebSocket. The first message is `{\"type\": \"hello\", \"apiVersion\": \"1\"}`; after it, one message per event, each with its `type` and an `at` timestamp. Bursts are coalesced, so a drag arrives a few times a second. Events name what changed; read the details with the commands. A browser cannot set a header on a WebSocket, so pass `?token=<key>`.\n\n- `dice.rolled`: `total`, `dice: [{die, value}]`, `roller`; a card's roll adds `label` and `modifier` (already in `total`).\n- `tokens.moved`: `tokens: [{id, cell, level}]`.\n- `tokens.changed`: Nothing; read `/v1/board`.\n- `decals.changed`: Nothing; read `/v1/board`.\n- `strokes.changed`: Nothing; read `/v1/board`.\n- `fog.changed`: Nothing; read `/v1/board`.\n- `levels.changed`: `activeLevel`.\n- `widgets.changed`: `tabs`: the tab ids whose widgets changed; read them with `get_widgets`.\n- `scene.switched`: `id`, `name`.\n- `cards.changed`: Nothing; read `get_cards`.\n- `smart.changed`: `ids`: the smart objects whose values changed; read them with `get_smart_object`.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "The key, for a client that cannot set a header.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "101": {
            "description": "Switched to a WebSocket."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "426": {
            "description": "Not a WebSocket request (`upgrade_required`)."
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "tags": [
          "MCP"
        ],
        "summary": "MCP endpoint",
        "description": "JSON-RPC 2.0 over streamable HTTP: `initialize`, `ping`, `tools/list` and `tools/call`, one message per request (no batches). Every command is a tool of the same name and arguments, and `initialize` returns instructions for the client. Notifications are answered `202` with no body.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "const": "2.0"
                  },
                  "id": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      }
                    ]
                  },
                  "method": {
                    "type": "string",
                    "example": "tools/call"
                  },
                  "params": {
                    "type": "object"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The JSON-RPC answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string",
                      "const": "2.0"
                    },
                    "id": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        }
                      ]
                    },
                    "result": {
                      "type": "object"
                    },
                    "error": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "A notification; nothing to answer."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/get_guide": {
      "post": {
        "operationId": "get_guide",
        "tags": [
          "Guides & Help"
        ],
        "summary": "get_guide",
        "description": "The reference guides, served as text. With no topic: the topics and their sections. Topics: board_games, smart_objects, commands. Read board_games before building a board game, and smart_objects before writing a smart object manifest — they hold the whole format and worked examples to copy. A section is named by its heading, or any part of it.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "topic": {
                    "type": "string",
                    "enum": [
                      "board_games",
                      "smart_objects",
                      "commands"
                    ],
                    "description": "Which guide. Leave out to list them."
                  },
                  "section": {
                    "type": "string",
                    "description": "One section of it, by its heading. Default: the whole guide."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_help": {
      "post": {
        "operationId": "get_help",
        "tags": [
          "Guides & Help"
        ],
        "summary": "get_help",
        "description": "The app's own Help: the steps, each button named as on screen, and what to open first from where the player is. For any question about using Phroller, pass the player's words as query and answer from the page, quoting its button names — never from memory. id reads one page; neither lists them all.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "The player's question, in their words."
                  },
                  "id": {
                    "type": "string",
                    "description": "One page, by the id a search or the list gives."
                  },
                  "limit": {
                    "type": "integer",
                    "description": "Default 5, at most 10."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/show_help": {
      "post": {
        "operationId": "show_help",
        "tags": [
          "Guides & Help"
        ],
        "summary": "show_help",
        "description": "A Help page on the player's screen: walkthrough (default) lights each real button in turn as they go; page opens Help on it.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The page, by its id from get_help."
                  },
                  "show": {
                    "type": "string",
                    "enum": [
                      "walkthrough",
                      "page"
                    ],
                    "description": "Default walkthrough."
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_board": {
      "post": {
        "operationId": "get_board",
        "tags": [
          "Board"
        ],
        "summary": "get_board",
        "description": "Summarise the open map: grid, levels, overall bounds in points/cells, object summary counts, and per level every token (with size, facing, elevation), prop, decal, wall (with bounds), door and die (with its face value, storey, colour and material) with its id and position in grid cells.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "description": "Only this level (id or name)."
                  },
                  "region": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4,
                    "description": "[x, y, width, height]. Only objects inside this rectangle, in points."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/check_area": {
      "post": {
        "operationId": "check_area",
        "tags": [
          "Board"
        ],
        "summary": "check_area",
        "description": "Inspect a rectangle in points [x, y, w, h] to test if it is clear or overlaps any tokens, props, decals, walls or doors. Returns clear: true or the specific overlapping objects. Ideal for spatial verification before placing rooms or tokens in JSON-only mode on any map size.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rect": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4,
                    "description": "[x, y, width, height]. Rectangle in points [x, y, w, h] to check."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  }
                },
                "required": [
                  "rect"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_scenes": {
      "post": {
        "operationId": "list_scenes",
        "tags": [
          "Board"
        ],
        "summary": "list_scenes",
        "description": "The maps in the open collection, in tab order, with the one on screen marked active. switch_scene takes an id or name from here.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/switch_scene": {
      "post": {
        "operationId": "switch_scene",
        "tags": [
          "Board"
        ],
        "summary": "switch_scene",
        "description": "Open another map from list_scenes, by id or name.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scene": {
                    "type": "string",
                    "description": "A map id or name."
                  }
                },
                "required": [
                  "scene"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/new_scene": {
      "post": {
        "operationId": "new_scene",
        "tags": [
          "Board"
        ],
        "summary": "new_scene",
        "description": "Open a new, empty map in its own tab and switch to it.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/undo_last": {
      "post": {
        "operationId": "undo_last",
        "tags": [
          "Board"
        ],
        "summary": "undo_last",
        "description": "Take back the most recent change made through this API. Level and environment changes, dice moved or deleted, and the card game's own actions — dealing, moving cards, flipping, shuffling — cannot be undone this way; laying out, moving and deleting piles can.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_camera": {
      "post": {
        "operationId": "set_camera",
        "tags": [
          "Camera & Screenshots"
        ],
        "summary": "set_camera",
        "description": "Aim your own camera as screenshot does, without a picture; fromUser starts from the player's view. target user moves the player's own screen instead, ending its auto-rotate or follow — only when they ask.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "center": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells."
                  },
                  "zoom": {
                    "type": "number"
                  },
                  "looking": {
                    "type": "string",
                    "enum": [
                      "north",
                      "north-east",
                      "east",
                      "south-east",
                      "south",
                      "south-west",
                      "west",
                      "north-west"
                    ],
                    "description": "Direction at the top of the picture."
                  },
                  "yaw": {
                    "type": "number",
                    "description": "Degrees; looking is easier."
                  },
                  "pitch": {
                    "type": "number",
                    "description": "0 is straight down, to 85; 45–75 shows heights and sides."
                  },
                  "fromUser": {
                    "type": "boolean",
                    "description": "Start from the player's view."
                  },
                  "target": {
                    "type": "string",
                    "enum": [
                      "headless",
                      "user"
                    ],
                    "description": "Default headless."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_assets": {
      "post": {
        "operationId": "list_assets",
        "tags": [
          "Assets"
        ],
        "summary": "list_assets",
        "description": "The token, prop and decal collections on this machine, with the asset ids place_tokens and place_decals accept, and the dice place_dice accepts: the active dice tab's first, then every other stock shape.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/search_library": {
      "post": {
        "operationId": "search_library",
        "tags": [
          "Assets"
        ],
        "summary": "search_library",
        "description": "Search the free online library of 3D models and map images by name, author and tag. Returns items with their download url, author, and artistUrl. Forward author and artistUrl to import_asset so creator attribution is preserved in Credits.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "Words to match."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "model",
                      "image",
                      "any"
                    ],
                    "description": "Default any."
                  },
                  "limit": {
                    "type": "integer",
                    "description": "Default 20, at most 100."
                  }
                },
                "required": [
                  "query"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/import_asset": {
      "post": {
        "operationId": "import_asset",
        "tags": [
          "Assets"
        ],
        "summary": "import_asset",
        "description": "Bring an image (PNG, JPEG, WebP, GIF), a 3D model (GLB, GLTF) or a PDF into the app and get back an asset id. Give exactly one of url, path, or uploadId (from POST /v1/upload). When downloading or importing assets from the internet or search_library, always pass artist and artistUrl so creator attribution is preserved in Credits. Imported assets are filed in an \"Imported\" collection.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "An http(s) url the app downloads."
                  },
                  "path": {
                    "type": "string",
                    "description": "A local file path. Not available in sandboxed builds; use url or uploadId there."
                  },
                  "uploadId": {
                    "type": "string",
                    "description": "An id returned by POST /v1/upload."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "image",
                      "model",
                      "pdf"
                    ],
                    "description": "Normally inferred from the file."
                  },
                  "name": {
                    "type": "string",
                    "description": "A display name. Defaults to the file name."
                  },
                  "artist": {
                    "type": "string",
                    "description": "Credit for the art/model creator. Always provide when downloading or importing assets from the internet (e.g. \"Poly by Google\", artist name, or username)."
                  },
                  "artistUrl": {
                    "type": "string",
                    "description": "Link to creator portfolio or asset source website (e.g. \"https://poly.pizza\" or art page)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/place_tokens": {
      "post": {
        "operationId": "place_tokens",
        "tags": [
          "Building"
        ],
        "summary": "place_tokens",
        "description": "Put tokens and props on the board. asset is an id from import_asset or list_assets. A 3D model stands as itself; an image becomes a standing paper mini (form token) or a standing card (form prop). Returns each new instance id.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tokens": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "asset": {
                          "type": "string",
                          "description": "Asset id."
                        },
                        "cell": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in cells. Where it stands."
                        },
                        "level": {
                          "type": "string",
                          "description": "Level id or name. Default: the active level."
                        },
                        "facing": {
                          "type": "number",
                          "description": "Compass bearing the token's front points toward, in degrees clockwise from north: 0 north, 90 east, 180 south, 270 west. North is -y, the top of the map (cell y decreases going north). The front is the way the token walks when dragged and the way its facing tick points; for a 3D model that is its +Z axis (glTF forward) unless its owner re-aimed the front on the board. Omit to keep the asset's own orientation."
                        },
                        "form": {
                          "type": "string",
                          "enum": [
                            "token",
                            "prop"
                          ],
                          "description": "token (a character, default) or prop (scenery)."
                        },
                        "label": {
                          "type": "string",
                          "description": "Name shown on the token."
                        },
                        "size": {
                          "type": "number",
                          "description": "Width in cells for an image. Default 1."
                        }
                      },
                      "required": [
                        "asset",
                        "cell"
                      ]
                    },
                    "description": "What to place."
                  }
                },
                "required": [
                  "tokens"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/place_decals": {
      "post": {
        "operationId": "place_decals",
        "tags": [
          "Building"
        ],
        "summary": "place_decals",
        "description": "Lay images, PDF pages, text or drawings flat on the board — map art, floor tiles, rugs, signs, handouts. rect is in points, so a map covering cells 0–19 by 0–14 is rect [0, 0, 20, 15]. Images stretch to fill rect; text, PDF and drawings keep their proportions and take rect's width. Instead of rect, cell puts a decal's centre in that cell, size cells wide — and a hex-shaped drawing fills exactly that many hex cells, so tiles for a hex board game are one place_decals entry each, by cell. A drawing is vector art (SVG-style paths, shapes, strokes and text) cut to a shape: rect, rounded, circle, hex (pointy top, the board's hex grid), hexFlat, or an outline of your own. Placing a drawing answers with a picture of it on the board: check it.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decals": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "asset": {
                          "type": "string",
                          "description": "Asset id of an image or PDF."
                        },
                        "text": {
                          "type": "string",
                          "description": "Text to show instead of an asset."
                        },
                        "drawing": {
                          "type": "object",
                          "description": "Vector art to draw instead of an asset. Its own coordinates: a width x height frame (default 1000 x 1000; a pointy hex 1000 x 1154.7, a flat hex 1154.7 x 1000 — leave them out and the shape's regular frame is used), x right and y down, the shape filling the frame and everything outside it see-through. Fields: shape (\"rect\", \"rounded\", \"circle\", \"hex\", \"hexFlat\", or {\"kind\": \"rounded\", \"radius\": 0–0.5} or {\"kind\": \"outline\", \"outline\": {\"points\": [[x, y, \"corner\"|\"curve\"], ...]}} in thousandths of the frame); background (fill inside the shape); border and borderWidth (a rim just inside the edge, in frame units); snap (true: when a player drags it and lets go, it lands on the nearest cell centre; default false); elements, drawn in order, each one of: {\"type\": \"path\", \"d\": SVG path data}, {\"type\": \"polygon\"|\"polyline\", \"points\": [x, y, x, y, ...]}, {\"type\": \"rect\", \"x\", \"y\", \"w\", \"h\", \"r\" (corner radius)}, {\"type\": \"ellipse\", \"cx\", \"cy\", \"rx\", \"ry\"}, {\"type\": \"circle\", \"cx\", \"cy\", \"r\"}, {\"type\": \"stroke\", \"points\": [...]} (a smooth freehand line), {\"type\": \"text\", \"x\", \"y\" (its centre), \"text\", \"size\", \"bold\", \"italic\", \"align\" (left|center|right)}. Each takes fill and stroke (#RRGGBB or #AARRGGBB; leave one out for none), width (stroke width) and opacity (0–1). Text is filled in fill; a stroke outlines it.",
                          "properties": {
                            "width": {
                              "type": "number"
                            },
                            "height": {
                              "type": "number"
                            },
                            "shape": {
                              "type": [
                                "string",
                                "object"
                              ]
                            },
                            "background": {
                              "type": "string",
                              "description": "#RRGGBB or #AARRGGBB."
                            },
                            "border": {
                              "type": "string",
                              "description": "#RRGGBB or #AARRGGBB."
                            },
                            "borderWidth": {
                              "type": "number"
                            },
                            "snap": {
                              "type": "boolean"
                            },
                            "elements": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "description": "One of: {\"type\": \"path\", \"d\": SVG path data}, {\"type\": \"polygon\"|\"polyline\", \"points\": [x, y, x, y, ...]}, {\"type\": \"rect\", \"x\", \"y\", \"w\", \"h\", \"r\" (corner radius)}, {\"type\": \"ellipse\", \"cx\", \"cy\", \"rx\", \"ry\"}, {\"type\": \"circle\", \"cx\", \"cy\", \"r\"}, {\"type\": \"stroke\", \"points\": [...]} (a smooth freehand line), {\"type\": \"text\", \"x\", \"y\" (its centre), \"text\", \"size\", \"bold\", \"italic\", \"align\" (left|center|right)}. Each takes fill and stroke (#RRGGBB or #AARRGGBB; leave one out for none), width (stroke width) and opacity (0–1). Text is filled in fill; a stroke outlines it.",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "enum": [
                                      "path",
                                      "stroke",
                                      "polygon",
                                      "polyline",
                                      "rect",
                                      "ellipse",
                                      "circle",
                                      "text"
                                    ]
                                  }
                                },
                                "required": [
                                  "type"
                                ]
                              }
                            }
                          }
                        },
                        "rect": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 4,
                          "maxItems": 4,
                          "description": "[x, y, width, height]. Where it lies, in points. This or cell."
                        },
                        "cell": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in cells. Centre it in this cell, instead of rect."
                        },
                        "size": {
                          "type": "number",
                          "description": "With cell: width in cells (a hex drawing: hex cells across). Default 1."
                        },
                        "rotation": {
                          "type": "number",
                          "description": "Degrees clockwise. Default 0."
                        },
                        "level": {
                          "type": "string",
                          "description": "Level id or name. Default: the active level."
                        },
                        "aboveGrid": {
                          "type": "boolean",
                          "description": "Draw over the grid lines. Default true; false for a map the grid should show through."
                        },
                        "locked": {
                          "type": "boolean",
                          "description": "Stop it being dragged by accident. Default false."
                        },
                        "opacity": {
                          "type": "number",
                          "description": "0–1. Default 1."
                        },
                        "page": {
                          "type": "integer",
                          "description": "PDF page, from 1. Default 1."
                        },
                        "textColor": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "backgroundColor": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "fontSize": {
                          "type": "number",
                          "description": "Default 64."
                        },
                        "bold": {
                          "type": "boolean"
                        },
                        "italic": {
                          "type": "boolean"
                        },
                        "label": {
                          "type": "string",
                          "description": "A name for it."
                        }
                      }
                    },
                    "description": "What to lay down."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "When any of them is a drawing, answer with a picture of what was laid down, as the board shows it. Default true."
                  }
                },
                "required": [
                  "decals"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/draw_walls": {
      "post": {
        "operationId": "draw_walls",
        "tags": [
          "Building"
        ],
        "summary": "draw_walls",
        "description": "Draw walls, doors and rooms, in points (cell corners). A wall is a polyline; a door is a two-point segment that swings open about its first point; a room is a rectangle of walls with gaps filled by doors. height is in cells (0 draws a flat line on the floor).\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "walls": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "points": {
                          "type": "array",
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            },
                            "minItems": 2,
                            "maxItems": 2,
                            "description": "[x, y] in points."
                          },
                          "description": "Two or more points."
                        },
                        "closed": {
                          "type": "boolean",
                          "description": "Join the last point back to the first."
                        },
                        "height": {
                          "type": "number",
                          "description": "0–10 cells. Default 2."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "width": {
                          "type": "number",
                          "description": "Line width in pixels. Default 6."
                        }
                      },
                      "required": [
                        "points"
                      ]
                    }
                  },
                  "doors": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in points."
                        },
                        "to": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in points."
                        },
                        "height": {
                          "type": "number",
                          "description": "Default: the same as walls, 2."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "open": {
                          "type": "number",
                          "description": "Degrees swung open. Default 0 (shut)."
                        }
                      },
                      "required": [
                        "from",
                        "to"
                      ]
                    }
                  },
                  "rooms": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "rect": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 4,
                          "maxItems": 4,
                          "description": "[x, y, width, height]. The room's outline, in points."
                        },
                        "height": {
                          "type": "number",
                          "description": "Default 2."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "doors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "side": {
                                "type": "string",
                                "enum": [
                                  "north",
                                  "east",
                                  "south",
                                  "west"
                                ]
                              },
                              "at": {
                                "type": "number",
                                "description": "Offset along that side from its north or west end, in cells."
                              },
                              "width": {
                                "type": "number",
                                "description": "Default 1."
                              }
                            },
                            "required": [
                              "side",
                              "at"
                            ]
                          },
                          "description": "Doorways in the room's walls."
                        }
                      },
                      "required": [
                        "rect"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/update_objects": {
      "post": {
        "operationId": "update_objects",
        "tags": [
          "Building"
        ],
        "summary": "update_objects",
        "description": "Change or delete things already on the board, by the ids get_board and the place commands return. Tokens take cell, facing, label, level, size, elevation, statuses (or addStatuses/removeStatuses), locked, collides, color, description, notes and claimed; decals take rect or cell, rotation, opacity, locked, level, label, aboveGrid, page (PDF), text styling (text, textColor, backgroundColor, fontSize, bold, italic), and for a drawing, drawing (replaces it whole) or addElements; doors take open (swing in degrees); walls take color and height; dice take cell, level, color, textColor, material, size, value (face showing on top), locked, gmOnly, and look for anything else about how a die looks or behaves (the d4 result overlay, pips, glow, cel shading, rounding, font, spindown, physics). delete removes any of them. Moving, restyling or deleting a die cannot be undone.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "updates": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "A token instance id, decal id, wall/door id or die id."
                        },
                        "delete": {
                          "type": "boolean"
                        },
                        "cell": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in cells."
                        },
                        "facing": {
                          "type": "number",
                          "description": "Compass bearing the token's front points toward, in degrees clockwise from north: 0 north, 90 east, 180 south, 270 west. North is -y, the top of the map (cell y decreases going north). The front is the way the token walks when dragged and the way its facing tick points; for a 3D model that is its +Z axis (glTF forward) unless its owner re-aimed the front on the board. Omit to keep the asset's own orientation."
                        },
                        "label": {
                          "type": "string"
                        },
                        "level": {
                          "type": "string",
                          "description": "Move to this level (id or name)."
                        },
                        "rect": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 4,
                          "maxItems": 4,
                          "description": "[x, y, width, height]"
                        },
                        "rotation": {
                          "type": "number",
                          "description": "Degrees."
                        },
                        "opacity": {
                          "type": "number",
                          "description": "0–1."
                        },
                        "locked": {
                          "type": "boolean"
                        },
                        "open": {
                          "type": "number",
                          "description": "A door's swing in degrees."
                        },
                        "size": {
                          "type": "number",
                          "description": "Size in cells for a token (e.g. 1.0, 2.0), or scale multiplier for a die (0.25–10)."
                        },
                        "statuses": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Token: replaces its statuses. Ids or names; a name from a stock list is switched on for the map."
                        },
                        "addStatuses": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Token: statuses to add."
                        },
                        "removeStatuses": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Token: statuses to take off."
                        },
                        "collides": {
                          "type": "boolean",
                          "description": "Token: false lets things pass through it."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "textColor": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "material": {
                          "type": "string",
                          "enum": [
                            "plastic",
                            "glass",
                            "metal",
                            "wood",
                            "random"
                          ],
                          "description": "Die material: plastic, glass, metal, wood, or random."
                        },
                        "value": {
                          "type": [
                            "integer",
                            "string"
                          ],
                          "description": "Die: face value showing on top (e.g. 20, 6, \"sword\")."
                        },
                        "elevation": {
                          "type": "number",
                          "description": "Token: elevation in cells above the level floor."
                        },
                        "gmOnly": {
                          "type": "boolean",
                          "description": "Die: everyone sees it, only the GM can pick it up."
                        },
                        "aboveGrid": {
                          "type": "boolean",
                          "description": "Decal: true renders over grid lines, false under."
                        },
                        "backgroundColor": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "fontSize": {
                          "type": "number",
                          "description": "Text decal: font size in pixels on the plate."
                        },
                        "bold": {
                          "type": "boolean",
                          "description": "Text decal: bold."
                        },
                        "italic": {
                          "type": "boolean",
                          "description": "Text decal: italic."
                        },
                        "height": {
                          "type": "number",
                          "description": "Wall: visual height in cells (0–10)."
                        },
                        "description": {
                          "type": "string",
                          "description": "Token: its description."
                        },
                        "notes": {
                          "type": "string",
                          "description": "Notepad or book prop: the text on its page."
                        },
                        "claimed": {
                          "type": "boolean",
                          "description": "Token: true claims it for this player, false releases it."
                        },
                        "page": {
                          "type": "integer",
                          "description": "PDF decal: go to this page (1 is the first)."
                        },
                        "text": {
                          "type": "string",
                          "description": "Text decal: replace its text."
                        },
                        "drawing": {
                          "type": "object",
                          "properties": {},
                          "description": "Drawing decal: its whole new drawing, in the form place_decals' drawing takes."
                        },
                        "addElements": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "description": "One of: {\"type\": \"path\", \"d\": SVG path data}, {\"type\": \"polygon\"|\"polyline\", \"points\": [x, y, x, y, ...]}, {\"type\": \"rect\", \"x\", \"y\", \"w\", \"h\", \"r\" (corner radius)}, {\"type\": \"ellipse\", \"cx\", \"cy\", \"rx\", \"ry\"}, {\"type\": \"circle\", \"cx\", \"cy\", \"r\"}, {\"type\": \"stroke\", \"points\": [...]} (a smooth freehand line), {\"type\": \"text\", \"x\", \"y\" (its centre), \"text\", \"size\", \"bold\", \"italic\", \"align\" (left|center|right)}. Each takes fill and stroke (#RRGGBB or #AARRGGBB; leave one out for none), width (stroke width) and opacity (0–1). Text is filled in fill; a stroke outlines it.",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "path",
                                  "stroke",
                                  "polygon",
                                  "polyline",
                                  "rect",
                                  "ellipse",
                                  "circle",
                                  "text"
                                ]
                              }
                            },
                            "required": [
                              "type"
                            ]
                          },
                          "description": "Drawing decal: elements to draw on top of what it has."
                        },
                        "look": {
                          "type": "object",
                          "properties": {},
                          "description": "Die: fields to change. Only the fields given change. Colours take #RRGGBB; null resets a colour, font or pip icon. Fields: name (Its display name.); baseColor (The shape's stock colour.); customColor (Body colour; null for the stock colour.); customTextColor (Number/pip colour; null for automatic contrast.); material (plastic, glass, metal or wood.); scale (Size multiplier, 0.25–10.); usePips (Dots instead of numbers (a d6 especially).); pipScale (Pip size multiplier.); pipOffset (Pip padding from the face edge.); pipIcon (An icon to use as the pip; null for dots.); isRounded (Rounded edges.); roundingAmount (How rounded, 0–1.); strokeWidth (Outline width.); hasGlow (Glow around the die.); glowIntensity (Glow strength, 0–1.); glowColor (Glow colour; null for the body colour.); isCelShaded (Cartoon outline shading.); celShadingThickness (Cartoon outline thickness multiplier.); celShadingColor (Cartoon outline colour.); isSpindown (Spindown numbering: consecutive numbers on adjacent faces.); fontFamily (The numbers' font; null for the tab's.); showD4CornerResult (d4 only: the large result number shown at the die's top corner.); d10PolesH (d10 only: how pointed its poles are.); customFaces (Per-face text or icons, keyed by face index.); useMaterialPhysics (Let the material change weight, bounce and grip.); mass (Weight.); bounciness (Bounce, 0–1.); friction (Grip, 0–1.); drag (Air drag, 0–1.); slidingDamping (How fast a slide slows, 0–1.)"
                        }
                      },
                      "required": [
                        "id"
                      ]
                    }
                  }
                },
                "required": [
                  "updates"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_levels": {
      "post": {
        "operationId": "set_levels",
        "tags": [
          "Building"
        ],
        "summary": "set_levels",
        "description": "Add, edit or remove levels (floors) and choose which one is active. elevation is in cells above the ground floor. A level is only removed once nothing is on it, and the last one never is.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "add": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "elevation": {
                          "type": "number",
                          "description": "Cells. Default: 3 above the highest."
                        },
                        "gmOnly": {
                          "type": "boolean",
                          "description": "Hidden from players."
                        }
                      },
                      "required": [
                        "name"
                      ]
                    }
                  },
                  "update": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "level": {
                          "type": "string",
                          "description": "Level id or name."
                        },
                        "name": {
                          "type": "string"
                        },
                        "elevation": {
                          "type": "number"
                        },
                        "gmOnly": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "level"
                      ]
                    }
                  },
                  "remove": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "description": "Level id or name."
                    },
                    "description": "Levels to delete. Each must be empty: move or delete what is on it first."
                  },
                  "active": {
                    "type": "string",
                    "description": "Make this level (id or name) the active one."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_fog": {
      "post": {
        "operationId": "set_fog",
        "tags": [
          "Building"
        ],
        "summary": "set_fog",
        "description": "Fog of war on one level: cover or uncover all of it, then reveal or hide rectangles (in points).\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "covered": {
                    "type": "boolean",
                    "description": "true covers the whole level, false clears it."
                  },
                  "reveal": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      },
                      "minItems": 4,
                      "maxItems": 4,
                      "description": "[x, y, width, height]"
                    },
                    "description": "Rectangles to uncover."
                  },
                  "hide": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      },
                      "minItems": 4,
                      "maxItems": 4,
                      "description": "[x, y, width, height]"
                    },
                    "description": "Rectangles to cover."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_environment": {
      "post": {
        "operationId": "set_environment",
        "tags": [
          "Building"
        ],
        "summary": "set_environment",
        "description": "The map's grid, background, horizon and sun.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "gridType": {
                    "type": "string",
                    "enum": [
                      "square",
                      "hex",
                      "none"
                    ]
                  },
                  "gridStyle": {
                    "type": "string",
                    "enum": [
                      "line",
                      "point"
                    ]
                  },
                  "lineStyle": {
                    "type": "string",
                    "enum": [
                      "solid",
                      "dashed",
                      "dotted"
                    ]
                  },
                  "gridColor": {
                    "type": "string",
                    "description": "#RRGGBB or #AARRGGBB."
                  },
                  "background": {
                    "type": "string",
                    "enum": [
                      "solid",
                      "radial_gradient",
                      "starry"
                    ]
                  },
                  "backgroundColor": {
                    "type": "string",
                    "description": "#RRGGBB or #AARRGGBB."
                  },
                  "horizon": {
                    "type": "string",
                    "enum": [
                      "match_board",
                      "solid",
                      "radial_gradient",
                      "starry"
                    ]
                  },
                  "horizonColor": {
                    "type": "string",
                    "description": "#RRGGBB or #AARRGGBB."
                  },
                  "sunAzimuth": {
                    "type": "number",
                    "description": "Compass bearing of the sun, degrees."
                  },
                  "sunElevation": {
                    "type": "number",
                    "description": "Height of the sun, degrees, 7–90."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_credits": {
      "post": {
        "operationId": "set_credits",
        "tags": [
          "Building"
        ],
        "summary": "set_credits",
        "description": "The map's Credits, which travel with its scenario. remove (id or label) runs first.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "add": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string"
                        },
                        "artist": {
                          "type": "string"
                        },
                        "artistUrl": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "label"
                      ]
                    }
                  },
                  "remove": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/ping": {
      "post": {
        "operationId": "ping",
        "tags": [
          "Playing"
        ],
        "summary": "ping",
        "description": "Point something out: the ripple a player makes with the ping tool, seen by everyone at the table. Give a cell (its centre) or a point.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells."
                  },
                  "point": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in points."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "color": {
                    "type": "string",
                    "description": "#RRGGBB or #AARRGGBB."
                  },
                  "label": {
                    "type": "string",
                    "description": "Text under the ripple. Default: this player's name."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/chat": {
      "post": {
        "operationId": "chat",
        "tags": [
          "Playing"
        ],
        "summary": "chat",
        "description": "Post to the table's chat. With token, it is said by that token and shown in a bubble over it; with narrator, it is shown in the middle of the screen. name is who it is from, as the chat panel's custom name is; the default is the token's name, or this player's.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "token": {
                    "type": "string",
                    "description": "A token instance id to speak as."
                  },
                  "narrator": {
                    "type": "boolean",
                    "description": "Show it as narration."
                  },
                  "name": {
                    "type": "string",
                    "description": "Who it is from."
                  }
                },
                "required": [
                  "message"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/measure": {
      "post": {
        "operationId": "measure",
        "tags": [
          "Playing"
        ],
        "summary": "measure",
        "description": "Measure between two points and, unless show is false, lay a ruler on the board for everyone to see. Returns the distance in cells and in the map's own units (e.g. \"35 ft\"). clear takes every ruler off first.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in points."
                  },
                  "to": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in points."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "color": {
                    "type": "string",
                    "description": "#RRGGBB or #AARRGGBB."
                  },
                  "show": {
                    "type": "boolean",
                    "description": "Lay a ruler down. Default true."
                  },
                  "clear": {
                    "type": "boolean",
                    "description": "Remove every ruler first."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/draw": {
      "post": {
        "operationId": "draw",
        "tags": [
          "Playing"
        ],
        "summary": "draw",
        "description": "Draw flat ink on the floor, like the pen: freehand lines, rectangles and circles, in points. gmOnly puts it on the GM's private notes layer, which only the GM sees. onTop draws it over the fog and every model. Returns the stroke ids; update_objects deletes them.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "strokes": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "shape": {
                          "type": "string",
                          "enum": [
                            "line",
                            "rect",
                            "circle"
                          ],
                          "description": "Default line."
                        },
                        "points": {
                          "type": "array",
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            },
                            "minItems": 2,
                            "maxItems": 2,
                            "description": "[x, y] in points."
                          },
                          "description": "line: two or more points."
                        },
                        "rect": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 4,
                          "maxItems": 4,
                          "description": "[x, y, width, height]. rect: the rectangle."
                        },
                        "center": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in points."
                        },
                        "radius": {
                          "type": "number",
                          "description": "circle: radius in cells."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB or #AARRGGBB."
                        },
                        "width": {
                          "type": "number",
                          "description": "Line width in pixels, 1–60. Default 4."
                        },
                        "filled": {
                          "type": "boolean",
                          "description": "rect or circle: fill it."
                        },
                        "onTop": {
                          "type": "boolean",
                          "description": "Over the fog and the models."
                        },
                        "gmOnly": {
                          "type": "boolean",
                          "description": "On the GM's private notes layer."
                        }
                      }
                    }
                  }
                },
                "required": [
                  "strokes"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/place_dice": {
      "post": {
        "operationId": "place_dice",
        "tags": [
          "Dice"
        ],
        "summary": "place_dice",
        "description": "Set dice down on the board, seated and at rest (not thrown). type is a die id or name from list_assets' dice (d4, d6, d8, d10, d00, d12, d20 and the rest). count dice of that kind are stacked in the cell, and dice put in a cell that already holds dice go on top, so several entries naming one cell build a tower bottom to top. A die that cannot rest on the one below it (a d4's point) tumbles off. Each die comes up on a random face. Returns each die's id and storey (0 is on the floor).\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dice": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "description": "Die id or name, e.g. \"d20\"."
                        },
                        "cell": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 2,
                          "description": "[x, y] in cells. Where it stands."
                        },
                        "level": {
                          "type": "string",
                          "description": "Level id or name. Default: the active level."
                        },
                        "count": {
                          "type": "integer",
                          "description": "How many to stack here, 1–50. Default 1."
                        },
                        "style": {
                          "type": "string",
                          "enum": [
                            "tab",
                            "random"
                          ],
                          "description": "tab (default): as the active dice tab dresses it. random: a random material, colours, rounding, glow and font, different for every die."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "textColor": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "material": {
                          "type": "string",
                          "enum": [
                            "plastic",
                            "glass",
                            "metal",
                            "wood",
                            "random"
                          ],
                          "description": "Overrides the style's material; \"random\" picks one per die."
                        },
                        "size": {
                          "type": "number",
                          "description": "Size multiplier, 0.25–10. Default 1."
                        },
                        "gmOnly": {
                          "type": "boolean",
                          "description": "Everyone sees it, only the GM can pick it up. Default false."
                        },
                        "look": {
                          "type": "object",
                          "properties": {},
                          "description": "Anything else about its look, over the style. Only the fields given change. Colours take #RRGGBB; null resets a colour, font or pip icon. Fields: name (Its display name.); baseColor (The shape's stock colour.); customColor (Body colour; null for the stock colour.); customTextColor (Number/pip colour; null for automatic contrast.); material (plastic, glass, metal or wood.); scale (Size multiplier, 0.25–10.); usePips (Dots instead of numbers (a d6 especially).); pipScale (Pip size multiplier.); pipOffset (Pip padding from the face edge.); pipIcon (An icon to use as the pip; null for dots.); isRounded (Rounded edges.); roundingAmount (How rounded, 0–1.); strokeWidth (Outline width.); hasGlow (Glow around the die.); glowIntensity (Glow strength, 0–1.); glowColor (Glow colour; null for the body colour.); isCelShaded (Cartoon outline shading.); celShadingThickness (Cartoon outline thickness multiplier.); celShadingColor (Cartoon outline colour.); isSpindown (Spindown numbering: consecutive numbers on adjacent faces.); fontFamily (The numbers' font; null for the tab's.); showD4CornerResult (d4 only: the large result number shown at the die's top corner.); d10PolesH (d10 only: how pointed its poles are.); customFaces (Per-face text or icons, keyed by face index.); useMaterialPhysics (Let the material change weight, bounce and grip.); mass (Weight.); bounciness (Bounce, 0–1.); friction (Grip, 0–1.); drag (Air drag, 0–1.); slidingDamping (How fast a slide slows, 0–1.)"
                        }
                      },
                      "required": [
                        "type",
                        "cell"
                      ]
                    },
                    "description": "What to put down, in order."
                  }
                },
                "required": [
                  "dice"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/roll_dice": {
      "post": {
        "operationId": "roll_dice",
        "tags": [
          "Dice"
        ],
        "summary": "roll_dice",
        "description": "Throw dice on the board, the way a player flicks them: they tumble and land on whatever face the physics gives. Give dice to make new ones (same fields as place_dice, without cell) or ids to roll dice already on the board, or both. They land around cell, or the middle of the screen. By default waits for them to stop and returns each face and the total; wait: false returns at once with the ids, and the result arrives as a dice.rolled event and in get_rolls. A roll cannot be undone; delete its dice with update_objects.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dice": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "description": "Die id or name, e.g. \"d20\"."
                        },
                        "count": {
                          "type": "integer",
                          "description": "How many of this kind, 1–50. Default 1."
                        },
                        "style": {
                          "type": "string",
                          "enum": [
                            "tab",
                            "random"
                          ],
                          "description": "tab (default): as the active dice tab dresses it. random: a different random look for every die."
                        },
                        "color": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "textColor": {
                          "type": "string",
                          "description": "#RRGGBB, #AARRGGBB, or \"random\" for a different colour on every die."
                        },
                        "material": {
                          "type": "string",
                          "enum": [
                            "plastic",
                            "glass",
                            "metal",
                            "wood",
                            "random"
                          ]
                        },
                        "size": {
                          "type": "number",
                          "description": "Size multiplier, 0.25–10. Default 1."
                        },
                        "gmOnly": {
                          "type": "boolean",
                          "description": "Only the GM can pick it up afterwards."
                        },
                        "look": {
                          "type": "object",
                          "properties": {},
                          "description": "Anything else about its look, over the style. Only the fields given change. Colours take #RRGGBB; null resets a colour, font or pip icon. Fields: name (Its display name.); baseColor (The shape's stock colour.); customColor (Body colour; null for the stock colour.); customTextColor (Number/pip colour; null for automatic contrast.); material (plastic, glass, metal or wood.); scale (Size multiplier, 0.25–10.); usePips (Dots instead of numbers (a d6 especially).); pipScale (Pip size multiplier.); pipOffset (Pip padding from the face edge.); pipIcon (An icon to use as the pip; null for dots.); isRounded (Rounded edges.); roundingAmount (How rounded, 0–1.); strokeWidth (Outline width.); hasGlow (Glow around the die.); glowIntensity (Glow strength, 0–1.); glowColor (Glow colour; null for the body colour.); isCelShaded (Cartoon outline shading.); celShadingThickness (Cartoon outline thickness multiplier.); celShadingColor (Cartoon outline colour.); isSpindown (Spindown numbering: consecutive numbers on adjacent faces.); fontFamily (The numbers' font; null for the tab's.); showD4CornerResult (d4 only: the large result number shown at the die's top corner.); d10PolesH (d10 only: how pointed its poles are.); customFaces (Per-face text or icons, keyed by face index.); useMaterialPhysics (Let the material change weight, bounce and grip.); mass (Weight.); bounciness (Bounce, 0–1.); friction (Grip, 0–1.); drag (Air drag, 0–1.); slidingDamping (How fast a slide slows, 0–1.)"
                        }
                      },
                      "required": [
                        "type"
                      ]
                    },
                    "description": "New dice to throw."
                  },
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Dice already on the board to roll again."
                  },
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells. Where they land. Default: the middle of the screen."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "wait": {
                    "type": "boolean",
                    "description": "Wait for the dice to stop. Default true."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_rolls": {
      "post": {
        "operationId": "get_rolls",
        "tags": [
          "Dice"
        ],
        "summary": "get_rolls",
        "description": "The most recent rolls on this machine, newest first — the board's and the dice tray's, this player's and everyone else's: total, each die and its face, who rolled, and when.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "description": "Default 10, at most 100."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_dice_tab": {
      "post": {
        "operationId": "get_dice_tab",
        "tags": [
          "Dice"
        ],
        "summary": "get_dice_tab",
        "description": "One dice tab (get_widgets lists them) as the tray holds it: its name, style and colours, and each of its dice with its id, shape, and how its look differs from the stock die of that shape. lookFields says what every look field does. Use render_die to see a die.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tab": {
                    "type": "string",
                    "description": "The tab's id or name. Default: the tab on screen."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/edit_dice_tab": {
      "post": {
        "operationId": "edit_dice_tab",
        "tags": [
          "Dice"
        ],
        "summary": "edit_dice_tab",
        "description": "Change a dice tab with a list of ops, applied in order — all of them or none. Ops: {op:\"look\", die, look} (look is a patch of the die's saved look — customColor, material, customFaces, usePips, hasGlow, fontFamily, physics and the rest; get_dice_tab's lookFields says what each does); {op:\"add\", die} (a shape — d4, d6, d8, d10, d12, d20, d100 — or one of the tab's dice); {op:\"remove\", die}; {op:\"rename\", name}; {op:\"style\", style}; {op:\"color\", color?, secondary?}. Answers with the tab and a picture of the die changed, or of all its dice.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tab": {
                    "type": "string",
                    "description": "The tab's id or name. Default: the tab on screen."
                  },
                  "ops": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "op": {
                          "type": "string",
                          "description": "What to do."
                        }
                      },
                      "required": [
                        "op"
                      ]
                    },
                    "description": "The changes, in order."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture. Default true."
                  }
                },
                "required": [
                  "ops"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/render_die": {
      "post": {
        "operationId": "render_die",
        "tags": [
          "Dice"
        ],
        "summary": "render_die",
        "description": "A PNG of one die from a dice tab on its own — on a transparent ground, without the customizer round it — or of all the tab's dice side by side. face turns that face up towards the camera.\n\nReads; changes nothing on the map. Answers with a picture (`png`, base64), and is refused when pictures are off in Settings.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": true
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tab": {
                    "type": "string",
                    "description": "The tab's id or name. Default: the tab on screen."
                  },
                  "die": {
                    "type": "string",
                    "description": "One of the tab's dice, by id or name. Leave out for all of them."
                  },
                  "face": {
                    "type": "string",
                    "description": "The face to show up, by what it says: \"20\", \"6\"."
                  },
                  "maxSize": {
                    "type": "integer",
                    "description": "Longest edge in pixels, 128–1024. Default 512."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_widgets": {
      "post": {
        "operationId": "get_widgets",
        "tags": [
          "Widgets"
        ],
        "summary": "get_widgets",
        "description": "Widgets are the counters, bars, locks, lists, tables, notes and drawings on a dice tab. A token shows the widgets of the tab it is linked to, or its own private set. With no arguments, lists this machine's dice tabs: id, name, widget count, and the tokens linked to each. With token or tab, returns that tab's widgets in full, in the JSON set_widgets accepts, each with its index. Widgets belong to this machine: other players do not see them.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "A token instance id: that token's widgets."
                  },
                  "tab": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "A dice tab: its widgets. A tab id from get_widgets, or its name."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_widgets": {
      "post": {
        "operationId": "set_widgets",
        "tags": [
          "Widgets"
        ],
        "summary": "set_widgets",
        "description": "Change the widgets on a dice tab or a token. Target a token (by instance id), a tab, or, with neither, the active tab. Edits run in this order: widgets replaces the whole list, update changes widgets by index or label (set merges fields; delta adds to a counter or slider, e.g. -7 for damage), remove deletes them, add appends new ones. A widget is JSON with a type and the fields get_widgets shows; ids inside lists are filled in. A token with no widgets gets its own set the first time one is added. link shares a tab with the token instead; link with own copies that tab into a set of its own, so each goblin tracks its own health. Linking a token nobody has claimed claims it for this player, which is what shows its widgets on selection. Cannot be undone with undo_last. A player who is not the GM may change tabs and the tokens they own.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "A token instance id."
                  },
                  "tab": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "A dice tab. A tab id from get_widgets, or its name."
                  },
                  "link": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "Token only: link it to this tab. A tab id from get_widgets, or its name."
                  },
                  "own": {
                    "type": "boolean",
                    "description": "Token only: give it a private set, copied from link, else from the tab it is linked to now."
                  },
                  "unlink": {
                    "type": "boolean",
                    "description": "Token only: remove its link. A private set is deleted with it."
                  },
                  "visible": {
                    "type": "boolean",
                    "description": "Show or hide the tab's widgets."
                  },
                  "widgets": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "A widget: {\"type\": one of counter, slider, lock, list, table, notes, drawing, pdf, resetter, \"label\": ..., ...}. counter: value, allowNegativeNumbers, format (\"[value]\"). slider (a bar): value, totalValue, displayMode (fraction, pips). lock: isLocked. list: items [{components [{type (text, counter, checkbox), textValue, intValue, boolValue}]}]. table: columns [{label, type}], rows [{cells [...]}]. notes: pages [{title, content}]. widthMultiplier sizes it (1 is one slot).",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "counter",
                            "slider",
                            "lock",
                            "list",
                            "table",
                            "notes",
                            "drawing",
                            "pdf",
                            "resetter"
                          ]
                        },
                        "label": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "type"
                      ]
                    },
                    "description": "Replaces every widget."
                  },
                  "update": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "widget": {
                          "type": [
                            "integer",
                            "string"
                          ],
                          "description": "A widget's index, or its label."
                        },
                        "set": {
                          "type": "object",
                          "properties": {},
                          "description": "Fields to change, e.g. {\"value\": 12} or {\"label\": \"HP\", \"totalValue\": 30} or {\"isLocked\": true}."
                        },
                        "delta": {
                          "type": "integer",
                          "description": "Add to a counter's or slider's value; clamped to its range."
                        }
                      },
                      "required": [
                        "widget"
                      ]
                    }
                  },
                  "remove": {
                    "type": "array",
                    "items": {
                      "type": [
                        "integer",
                        "string"
                      ],
                      "description": "A widget's index, or its label."
                    }
                  },
                  "add": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "A widget: {\"type\": one of counter, slider, lock, list, table, notes, drawing, pdf, resetter, \"label\": ..., ...}. counter: value, allowNegativeNumbers, format (\"[value]\"). slider (a bar): value, totalValue, displayMode (fraction, pips). lock: isLocked. list: items [{components [{type (text, counter, checkbox), textValue, intValue, boolValue}]}]. table: columns [{label, type}], rows [{cells [...]}]. notes: pages [{title, content}]. widthMultiplier sizes it (1 is one slot).",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "counter",
                            "slider",
                            "lock",
                            "list",
                            "table",
                            "notes",
                            "drawing",
                            "pdf",
                            "resetter"
                          ]
                        },
                        "label": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "type"
                      ]
                    },
                    "description": "Appended, in order."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture of the widgets as they are drawn. Default true."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/render_widgets": {
      "post": {
        "operationId": "render_widgets",
        "tags": [
          "Widgets"
        ],
        "summary": "render_widgets",
        "description": "A PNG of a dice tab's or a token's widgets — counters, bars, locks, lists, tables, notes — drawn as the tray draws them, cropped to the widgets, without the rest of the screen.\n\nReads; changes nothing on the map. Answers with a picture (`png`, base64), and is refused when pictures are off in Settings.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": true
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "A token instance id: its widgets."
                  },
                  "tab": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "A dice tab. With neither, the tab on screen. A tab id from get_widgets, or its name."
                  },
                  "maxSize": {
                    "type": "integer",
                    "description": "Width in pixels, 256–1600. Default 720."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_token": {
      "post": {
        "operationId": "get_token",
        "tags": [
          "Library Tokens"
        ],
        "summary": "get_token",
        "description": "One token or prop from the library (list_assets lists them) as the token customizer holds it: its look — label, colours, size, each side's face, a model's colours and pose — and lookFields, what every look field does. Use render_token to see it.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "asset": {
                    "type": "string",
                    "description": "The token's or prop's asset id."
                  }
                },
                "required": [
                  "asset"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/new_token": {
      "post": {
        "operationId": "new_token",
        "tags": [
          "Library Tokens"
        ],
        "summary": "new_token",
        "description": "Make a token or prop in the library, filed in a collection (\"Imported\" unless named): a coin in its colour bearing its label, or what look makes it — a number, words or a symbol on each side. Every coin-shaped piece is one: a coin, a chit, a number chip on a tile. Answers with its asset id — ready for place_tokens — and a picture of it.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "description": "Its letters and its name."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "token",
                      "prop"
                    ],
                    "description": "Default token."
                  },
                  "collection": {
                    "type": "string",
                    "description": "The collection to file it in. Default \"Imported\"."
                  },
                  "look": {
                    "type": "object",
                    "properties": {},
                    "description": "How it looks, over a plain coin. Only the fields given change. Fields: label (The letters on a plain token, and its name in lists.); color (Body colour, #RRGGBB or #AARRGGBB.); textColor (Label and icon colour.); scale (Size on the board, in grid cells (0.25–20).); description (A note shown with it.); artist (Who made its art.); artistUrl (Where its artist is credited.); customFaces (What each side shows, keyed by side. A plain token is a coin: \"1\" is its face, and \"2\" (its underside) shows the same unless \"sync_sides\" is {\"type\": \"text\", \"text\": \"false\"}. A side is {\"type\": \"text\", \"text\": \"6\"} for words or a number, or {\"type\": \"icon\", \"icons\": [{\"type\": \"iconify\", \"iconifyName\": \"mdi:sword\"}]} for symbols (any Iconify name; \"layout\": cluster, row, column or grid for several). Draw symbols this way, never with emoji.); materialColors (A 3D model's colours by material name, each #RRGGBB.); rotation (A 3D model's turn, in radians.); pitch (A 3D model's tilt, in radians.); roll (A 3D model's roll, in radians.); lockYaw (Keeps the turn set above when placed.); lockPitch (Keeps the tilt.); lockRoll (Keeps the roll.); jointRotations (A rigged model's pose, by joint.); ignoreRootBone (A rigged model's root bone left out of the pose.); notePages (A notepad prop's pages.); activeNotePage (Which page a notepad shows.); noteStyle (A notepad's look: notepad, scroll, sign, …)"
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture. Default true."
                  }
                },
                "required": [
                  "label"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/edit_token": {
      "post": {
        "operationId": "edit_token",
        "tags": [
          "Library Tokens"
        ],
        "summary": "edit_token",
        "description": "Change a library token's or prop's look: look is a patch of its saved look (its fields are listed under look). Tokens already on the board keep their own copy — update_objects changes those. Answers with its look and a picture of it.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "asset": {
                    "type": "string",
                    "description": "The token's or prop's asset id."
                  },
                  "look": {
                    "type": "object",
                    "properties": {},
                    "description": "What to change: any of the look fields new_token lists, only those given."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture. Default true."
                  }
                },
                "required": [
                  "asset",
                  "look"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/render_token": {
      "post": {
        "operationId": "render_token",
        "tags": [
          "Library Tokens"
        ],
        "summary": "render_token",
        "description": "A PNG of a library token or prop on its own, drawn as the library and the customizer draw it, without them round it.\n\nReads; changes nothing on the map. Answers with a picture (`png`, base64), and is refused when pictures are off in Settings.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": true
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "asset": {
                    "type": "string",
                    "description": "The token's or prop's asset id."
                  },
                  "maxSize": {
                    "type": "integer",
                    "description": "Longest edge in pixels, 128–1024. Default 512."
                  }
                },
                "required": [
                  "asset"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_cards": {
      "post": {
        "operationId": "get_cards",
        "tags": [
          "Cards"
        ],
        "summary": "get_cards",
        "description": "The cards in play, as this player may see them: every pile on the table — where it lies in cells, which way it faces, its cards bottom to top — and every hand at the table, with the table log newest first. A card this player may not see has no name; its id is only good until its pile is next shuffled.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "log": {
                    "type": "integer",
                    "description": "How many table log lines. Default 20, at most 100."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_card_sets": {
      "post": {
        "operationId": "list_card_sets",
        "tags": [
          "Cards"
        ],
        "summary": "list_card_sets",
        "description": "The decks kept on this machine — Add Cards' \"Your Sets\" — which add_cards lays out by id or name.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/add_cards": {
      "post": {
        "operationId": "add_cards",
        "tags": [
          "Cards"
        ],
        "summary": "add_cards",
        "description": "Lay a new deck on the table, face down unless faceUp: a kept set from list_card_sets — a card design is one, by its name — the standard 52-card deck (54 with jokers), or one card per picture from import_asset. New cards are a card design (new_card_design); pictures are for decks that already exist as pictures, never a picture of a whole card. rows lays out only those cards of a design: one face-up supply pile per kind is a call each. A new deck is kept in Your Sets.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "set": {
                    "type": "string",
                    "description": "A kept set's id or name."
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "With a design's set: only these cards, by name or row id."
                  },
                  "standard": {
                    "type": "boolean",
                    "description": "The standard deck."
                  },
                  "jokers": {
                    "type": "boolean",
                    "description": "With standard: and two jokers."
                  },
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "image": {
                          "type": "string",
                          "description": "An asset id from import_asset."
                        },
                        "name": {
                          "type": "string",
                          "description": "Default: the picture's name."
                        }
                      },
                      "required": [
                        "image"
                      ]
                    },
                    "description": "One card per picture, bottom to top."
                  },
                  "name": {
                    "type": "string",
                    "description": "The deck's name. Default: the set's, or \"Cards\"."
                  },
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells. Where the bottom card's middle lies. Default: the middle of the screen."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "facing": {
                    "type": "number",
                    "description": "Compass bearing the cards' tops point toward, in degrees clockwise from north: 0 north, 90 east, 180 south, 270 west. Rounded to 5 degrees. Default north."
                  },
                  "faceUp": {
                    "type": "boolean",
                    "description": "Lay it face up."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/move_cards": {
      "post": {
        "operationId": "move_cards",
        "tags": [
          "Cards"
        ],
        "summary": "move_cards",
        "description": "Move cards from get_cards: onto a pile or into a hand (to: its id, or \"hand\" for this player's own), or onto the table as a new pile (cell). Drawing, playing, passing and stacking are all this. The table's rules decide who may move what.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Card ids, bottom to top."
                  },
                  "to": {
                    "type": "string",
                    "description": "A pile or hand id, or \"hand\"."
                  },
                  "index": {
                    "type": "integer",
                    "description": "With to: where in it, 0 the bottom. Default the top."
                  },
                  "place": {
                    "type": "string",
                    "enum": [
                      "top",
                      "bottom",
                      "random"
                    ],
                    "description": "With to: on top (the default), on the bottom, or shuffled in at places nobody can follow."
                  },
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells. A new pile here: where its bottom card's middle lies."
                  },
                  "level": {
                    "type": "string",
                    "description": "With cell: level id or name."
                  },
                  "facing": {
                    "type": "number",
                    "description": "Compass bearing the cards' tops point toward, in degrees clockwise from north: 0 north, 90 east, 180 south, 270 west. Rounded to 5 degrees. Default north."
                  },
                  "faceUp": {
                    "type": "boolean",
                    "description": "Turn every moved card this way up."
                  }
                },
                "required": [
                  "cards"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/deal_cards": {
      "post": {
        "operationId": "deal_cards",
        "tags": [
          "Cards"
        ],
        "summary": "deal_cards",
        "description": "Deal count cards off the top of a pile into each of hands in turn, a round at a time. With no hands, into this player's own: a draw.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pile": {
                    "type": "string",
                    "description": "A pile id from get_cards."
                  },
                  "count": {
                    "type": "integer",
                    "description": "Cards each. Default 1, at most 60."
                  },
                  "hands": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Hand ids. Default: this player's."
                  },
                  "faceUp": {
                    "type": "boolean",
                    "description": "Turn them this way up."
                  }
                },
                "required": [
                  "pile"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/flip_cards": {
      "post": {
        "operationId": "flip_cards",
        "tags": [
          "Cards"
        ],
        "summary": "flip_cards",
        "description": "Turn cards over. With pile and no faceUp, the whole pile is turned over as a deck is: its order reverses.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Card ids."
                  },
                  "pile": {
                    "type": "string",
                    "description": "A pile id."
                  },
                  "faceUp": {
                    "type": "boolean",
                    "description": "Turn them this way up. Default: each over."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/shuffle_pile": {
      "post": {
        "operationId": "shuffle_pile",
        "tags": [
          "Cards"
        ],
        "summary": "shuffle_pile",
        "description": "Shuffle a pile, squaring it into a neat deck.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pile": {
                    "type": "string",
                    "description": "A pile id."
                  }
                },
                "required": [
                  "pile"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/move_pile": {
      "post": {
        "operationId": "move_pile",
        "tags": [
          "Cards"
        ],
        "summary": "move_pile",
        "description": "Move, turn, lay out or pin a pile: cell puts its bottom card's middle there, facing turns it, layout spreads it out or squares it up, and pinned pins it where it lies or lets it go.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pile": {
                    "type": "string",
                    "description": "A pile id."
                  },
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name."
                  },
                  "facing": {
                    "type": "number",
                    "description": "Compass bearing the cards' tops point toward, in degrees clockwise from north: 0 north, 90 east, 180 south, 270 west. Rounded to 5 degrees. Default north."
                  },
                  "layout": {
                    "type": "string",
                    "enum": [
                      "stacked",
                      "spread"
                    ]
                  },
                  "pinned": {
                    "type": "boolean",
                    "description": "Pin it where it lies — a tap still opens it, and a drag that starts on it moves the board instead — or false to let it go. A pinned pile cannot be moved or turned until it is let go. Pinning is the GM's unless a pile's rules say otherwise."
                  }
                },
                "required": [
                  "pile"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/peek_cards": {
      "post": {
        "operationId": "peek_cards",
        "tags": [
          "Cards"
        ],
        "summary": "peek_cards",
        "description": "Look at the top count cards of a pile — every card when count is left out, which is searching it — without anyone else seeing them. Returns the cards seen, top first. Looking is the GM's on the table unless a pile's rules say otherwise; in a hand, its owner's.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pile": {
                    "type": "string",
                    "description": "A pile or hand id."
                  },
                  "count": {
                    "type": "integer",
                    "description": "How many off the top. Leave out to search it all."
                  }
                },
                "required": [
                  "pile"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/reveal_cards": {
      "post": {
        "operationId": "reveal_cards",
        "tags": [
          "Cards"
        ],
        "summary": "reveal_cards",
        "description": "Show cards this player can see to everyone at the table, or to one player (to: their player id).\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Card ids."
                  },
                  "to": {
                    "type": "string",
                    "description": "A player id. Default everyone."
                  }
                },
                "required": [
                  "cards"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/tap_cards": {
      "post": {
        "operationId": "tap_cards",
        "tags": [
          "Cards"
        ],
        "summary": "tap_cards",
        "description": "Turn cards sideways (tapped) or back upright; each to the other when tapped is left out.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Card ids."
                  },
                  "tapped": {
                    "type": "boolean",
                    "description": "Sideways, or upright."
                  }
                },
                "required": [
                  "cards"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/cut_pile": {
      "post": {
        "operationId": "cut_pile",
        "tags": [
          "Cards"
        ],
        "summary": "cut_pile",
        "description": "Cut a pile: at cards off the top go under the rest, at a random place when at is left out.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pile": {
                    "type": "string",
                    "description": "A pile id."
                  },
                  "at": {
                    "type": "integer",
                    "description": "How many off the top."
                  }
                },
                "required": [
                  "pile"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/delete_cards": {
      "post": {
        "operationId": "delete_cards",
        "tags": [
          "Cards"
        ],
        "summary": "delete_cards",
        "description": "Destroy cards, or a whole pile with its cards.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Card ids."
                  },
                  "pile": {
                    "type": "string",
                    "description": "A pile id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_card_designs": {
      "post": {
        "operationId": "list_card_designs",
        "tags": [
          "Card Designs"
        ],
        "summary": "list_card_designs",
        "description": "The card designs kept on this machine (Your Sets, made in the Card Builder), every widget kind a card is built from with the options a new one starts with, the card shapes, and the ready-made layouts.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_card_design": {
      "post": {
        "operationId": "get_card_design",
        "tags": [
          "Card Designs"
        ],
        "summary": "get_card_design",
        "description": "One card design as data: its size in millimetres and its plate in pixels (widget boxes are plate pixels, [x, y, width, height] from the top left), shape, palette, every widget with its kind, box and options, its fields, every card's values, its backs, and what Check finds — text that runs off or shrinks too far, a widget that draws nothing on some cards. Use render_card to see it.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "design": {
                    "type": "string",
                    "description": "The design's id, its set's id, or its name."
                  }
                },
                "required": [
                  "design"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/new_card_design": {
      "post": {
        "operationId": "new_card_design",
        "tags": [
          "Card Designs"
        ],
        "summary": "new_card_design",
        "description": "Make a card design, kept in Your Sets: a blank poker card, another shape or size, or a ready-made layout from list_card_designs. A design is how cards are made: one layout of widgets (title, text, stat, picture, icon, …) whose fields set_card_rows fills in per card — never a picture of a whole card, nor decals on the board. Answers with the design as get_card_design gives it and a picture of its first card.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What the design is called."
                  },
                  "shape": {
                    "type": "string",
                    "description": "A shape from list_card_designs. Default: a poker card."
                  },
                  "widthMm": {
                    "type": "number",
                    "description": "Width in millimetres (15–1000)."
                  },
                  "heightMm": {
                    "type": "number",
                    "description": "Height in millimetres (15–1000)."
                  },
                  "layout": {
                    "type": "string",
                    "description": "A layout id or name to start from."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture of the card. Default true."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/edit_card_design": {
      "post": {
        "operationId": "edit_card_design",
        "tags": [
          "Card Designs"
        ],
        "summary": "edit_card_design",
        "description": "Change a card design with a list of ops, applied in order — all of them or none. Ops: {op:\"add\", kind, box?, options?, name?, side?:\"front\"|\"back\"} (answers with the new widget ids in added); {op:\"set\", widget, options} (options merged; a text widget shows a card's field with options.field, its own words with options.text, or words with values written in with options.template, e.g. \"Costs {cost}\"); {op:\"move\", widget, box}; {op:\"remove\", widget}; {op:\"name\", widget, name}; {op:\"lock\", widget, locked}; {op:\"restack\", widget, by}; {op:\"palette\", name, color:\"#RRGGBB\"} (widgets read it as \"{palette.<name>}\"); {op:\"size\", widthMm, heightMm}; {op:\"shape\", shape, cornerRadius?}; {op:\"rename\", name}; {op:\"layout\", layout, takeShape?}. Boxes are plate pixels. A design open in the Card Builder changes there at once, as one undo step. Answers with the design, what Check finds, and a picture of the card.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "design": {
                    "type": "string",
                    "description": "The design's id, its set's id, or its name."
                  },
                  "ops": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "op": {
                          "type": "string",
                          "description": "What to do."
                        }
                      },
                      "required": [
                        "op"
                      ]
                    },
                    "description": "The changes, in order."
                  },
                  "card": {
                    "type": "integer",
                    "description": "Which card the answer pictures, from 0. Default 0."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture of the card. Default true."
                  }
                },
                "required": [
                  "design",
                  "ops"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/set_card_rows": {
      "post": {
        "operationId": "set_card_rows",
        "tags": [
          "Card Designs"
        ],
        "summary": "set_card_rows",
        "description": "Fill in a design's cards: rows of values by field name — a field the design has not got is made, typed by its values — with count (how many of that card) and back (a named back); or CSV text with a header row, as the Card Builder imports it. Replaces the cards unless replace is false. Answers with the design, what Check finds and a picture of the first card.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "design": {
                    "type": "string",
                    "description": "The design's id, its set's id, or its name."
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {}
                    },
                    "description": "One object per card: {name: \"Goblin\", hp: 5, count: 3}."
                  },
                  "csv": {
                    "type": "string",
                    "description": "The cards as CSV, a header row first."
                  },
                  "replace": {
                    "type": "boolean",
                    "description": "Replace the cards. Default true; false adds after them."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture of a card. Default true."
                  }
                },
                "required": [
                  "design"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/render_card": {
      "post": {
        "operationId": "render_card",
        "tags": [
          "Card Designs"
        ],
        "summary": "render_card",
        "description": "A PNG of a card design on its own — one card's front or back, or a sheet of every card — cut to its shape on a transparent ground, without the Card Builder round it. Drawn as the table draws it.\n\nReads; changes nothing on the map. Answers with a picture (`png`, base64), and is refused when pictures are off in Settings.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": true
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "design": {
                    "type": "string",
                    "description": "The design's id, its set's id, or its name."
                  },
                  "card": {
                    "type": "integer",
                    "description": "Which card, from 0. Default 0."
                  },
                  "back": {
                    "type": "boolean",
                    "description": "Its back rather than its front."
                  },
                  "backName": {
                    "type": "string",
                    "description": "With back: one of the design's named backs."
                  },
                  "sheet": {
                    "type": "boolean",
                    "description": "Every card in a grid, up to 36."
                  },
                  "maxSize": {
                    "type": "integer",
                    "description": "Longest edge in pixels, 128–2048. Default 768, 1536 for a sheet."
                  }
                },
                "required": [
                  "design"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/import_smart_package": {
      "post": {
        "operationId": "import_smart_package",
        "tags": [
          "Smart Objects"
        ],
        "summary": "import_smart_package",
        "description": "Add a smart object package to this machine's library (the decal tray's \"Smart Objects\" tab) and get back its package id. A smart object is a decal, token or prop with its own values, picture and buttons, that can lay out and shuffle pieces — a game's controller, a scoreboard, a door. Read get_guide smart_objects for the format before writing a manifest (and board_games before building a game). Either give a finished .phsmart file — exactly one of url, path or uploadId — or give its parts: manifest (the manifest.json object) and files, every picture it names, by path — a picture may be given as a drawing, which is painted into it. A manifest whose poster is not among the files gets one drawn from its plate. A manifest with problems is refused with every problem listed, each with where it is. Importing the same parts twice keeps one package: a package is named by its hash. Answers with a picture of the object and its pieces, labelled — check it before placing.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "An http(s) url of a .phsmart file."
                  },
                  "path": {
                    "type": "string",
                    "description": "A local .phsmart path. Not available in sandboxed builds."
                  },
                  "uploadId": {
                    "type": "string",
                    "description": "A .phsmart sent with POST /v1/upload."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture: the object, then each piece as its onPlace lays them out, as the board will draw them. Default true."
                  },
                  "manifest": {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": true,
                    "description": "The manifest, as manifest.json would hold it: format \"phroller.smart/1\", id (reverse-DNS), version, name, kind (\"decal\", \"token\" or \"prop\"; a token or prop needs a body with a .glb), plate [w, h] (at most 1024 each), state (its fields), view (layers: rect, text, image, bar, pips, button — no shapes; draw shapes as picture files), actions (steps; onPlace runs as it is placed), children (piece templates it may spawn, shuffle, tell and despawn). The whole format is get_guide smart_objects."
                  },
                  "files": {
                    "type": "object",
                    "properties": {},
                    "description": "With manifest: each file the package holds, by its path inside it (\"art/hex.png\"), as base64 text, as {\"uploadId\": \"…\"}, or — for a .png — as {\"drawing\": …, \"size\": longest side in pixels, default 512}, the drawing painted into the picture: the same vector drawing place_decals takes, clipped to its shape (a \"hex\" tile is see-through outside the hexagon); a .phcards as {\"cardSet\": a kept set} packs that deck, for the manifest's cards.",
                    "additionalProperties": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "uploadId": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "uploadId"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "drawing": {
                              "type": "object",
                              "description": "A drawing exactly as place_decals takes one (its fields are documented there)."
                            },
                            "size": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "drawing"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "cardSet": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "cardSet"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_smart_packages": {
      "post": {
        "operationId": "list_smart_packages",
        "tags": [
          "Smart Objects"
        ],
        "summary": "list_smart_packages",
        "description": "The smart object packages in this machine's library: each one's package id, tray asset id, name, version, actions and child templates.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/place_smart_object": {
      "post": {
        "operationId": "place_smart_object",
        "tags": [
          "Smart Objects"
        ],
        "summary": "place_smart_object",
        "description": "Put a smart object on the board, centred in a cell. Its onPlace action runs as it lands — spawning any pieces it lays out — and the answer says what it holds and what it spawned, with a picture of it and its pieces on the board. One undo step takes the object and its pieces back.\n\nWrites the map: refused when board edits are off, and for anybody but the map's GM.",
        "x-phroller": {
          "writes": true,
          "gmOnly": true,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "package": {
                    "type": "string",
                    "description": "A package id (sha256:…) or its tray asset id."
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Answer with a picture of it and its pieces on the board. Default true."
                  },
                  "cell": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[x, y] in cells. The cell to centre it in."
                  },
                  "level": {
                    "type": "string",
                    "description": "Level id or name. Default: the active level."
                  },
                  "width": {
                    "type": "number",
                    "description": "Its width in cells. Default: as the tray places it."
                  }
                },
                "required": [
                  "package",
                  "cell"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/list_smart_objects": {
      "post": {
        "operationId": "list_smart_objects",
        "tags": [
          "Smart Objects"
        ],
        "summary": "list_smart_objects",
        "description": "The smart objects on the board: id, name, package, cell, level, revision, and the size of each group of pieces it spawned. Pieces themselves (objects with a template) are left out unless includePieces is true.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "description": "Only this level (id or name)."
                  },
                  "includePieces": {
                    "type": "boolean",
                    "description": "Also list spawned pieces. Default false."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/get_smart_object": {
      "post": {
        "operationId": "get_smart_object",
        "tags": [
          "Smart Objects"
        ],
        "summary": "get_smart_object",
        "description": "One smart object: its values, the actions this player may press (with why one is greyed out), the fields they may edit, and the pieces it spawned, by group.\n\nReads; changes nothing on the map.",
        "x-phroller": {
          "writes": false,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The object's id."
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/press_smart_object": {
      "post": {
        "operationId": "press_smart_object",
        "tags": [
          "Smart Objects"
        ],
        "summary": "press_smart_object",
        "description": "Press one of a smart object's actions, as this machine's player — the same as tapping its button. Held to the action's who rule. Answers done (and the object as it now stands), asked (sent to the host, which runs objects at a table), or a refusal with why.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The object's id."
                  },
                  "action": {
                    "type": "string",
                    "description": "The action's name, as get_smart_object lists it."
                  },
                  "args": {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": true,
                    "description": "The action's arguments, if it takes any."
                  }
                },
                "required": [
                  "id",
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/edit_smart_object": {
      "post": {
        "operationId": "edit_smart_object",
        "tags": [
          "Smart Objects"
        ],
        "summary": "edit_smart_object",
        "description": "Set one of a smart object's fields directly, as its Edit… panel does — held to the field's edit rule.\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The object's id."
                  },
                  "field": {
                    "type": "string",
                    "description": "The field's name."
                  },
                  "value": {
                    "description": "The new value: a number, true/false or text."
                  }
                },
                "required": [
                  "id",
                  "field",
                  "value"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    },
    "/v1/grant_smart_object": {
      "post": {
        "operationId": "grant_smart_object",
        "tags": [
          "Smart Objects"
        ],
        "summary": "grant_smart_object",
        "description": "Grant permissions to a smart object on the board (e.g. tokens.write, board.read).\n\nWrites this machine's own things, or what any player may do: refused when board edits are off.",
        "x-phroller": {
          "writes": true,
          "gmOnly": false,
          "picture": false
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The object's id."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The scopes to grant (e.g. [\"tokens.write\", \"board.read\"])."
                  }
                },
                "required": [
                  "id",
                  "scopes"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The command ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Answer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Refused"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The key from **Settings › System › Companion API**."
      },
      "TokenQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "token",
        "description": "The key in the query string, for a `GET` from a client that cannot set a header."
      }
    },
    "schemas": {
      "Warnings": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "What the command noticed but did not refuse: words cut off by a shape, and keys it did not use, with the field each was probably meant to be.",
        "example": [
          "`tokens[0].facng` is not a field here, so it was ignored; did you mean `facing`?"
        ]
      },
      "Answer": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string",
            "const": "1"
          },
          "ok": {
            "type": "boolean",
            "const": true
          },
          "result": {
            "type": "object",
            "description": "The command's answer. What it holds is the command's own; `warnings` and `png` may come with any of them.",
            "properties": {
              "warnings": {
                "$ref": "#/components/schemas/Warnings"
              },
              "png": {
                "type": "string",
                "contentEncoding": "base64",
                "description": "A picture, when the command took one."
              }
            },
            "additionalProperties": true
          }
        },
        "required": [
          "apiVersion",
          "ok",
          "result"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string",
            "const": "1"
          },
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, to branch on: `invalid_arguments`, `unauthorized`, `read_only`, `screenshots_disabled`, `not_gm`, `limit_reached`, `not_found`, `unknown_command`, `no_board`, `busy`, `too_large`, `forbidden_host`, `forbidden_origin`, and others a command names."
              },
              "message": {
                "type": "string",
                "description": "For a person, and says what to do instead."
              },
              "warnings": {
                "$ref": "#/components/schemas/Warnings"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "apiVersion",
          "ok",
          "error"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A field is missing or malformed (`invalid_arguments`); the message names it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key, or the wrong one (`unauthorized`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Switched off or not allowed: `read_only`, `screenshots_disabled`, `not_gm`, `limit_reached`, `forbidden_host`, `forbidden_origin`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such object, level, asset or command (`not_found`, `unknown_command`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "No map is open, or it is still loading (`no_board`, `busy`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "The body is over the limit (`too_large`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Refused": {
        "description": "The command could not do what was asked; the code and message say why.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "x-phroller-events": [
    {
      "type": "dice.rolled",
      "payload": "`total`, `dice: [{die, value}]`, `roller`; a card's roll adds `label` and `modifier` (already in `total`)."
    },
    {
      "type": "tokens.moved",
      "payload": "`tokens: [{id, cell, level}]`."
    },
    {
      "type": "tokens.changed",
      "payload": "Nothing; read `/v1/board`."
    },
    {
      "type": "decals.changed",
      "payload": "Nothing; read `/v1/board`."
    },
    {
      "type": "strokes.changed",
      "payload": "Nothing; read `/v1/board`."
    },
    {
      "type": "fog.changed",
      "payload": "Nothing; read `/v1/board`."
    },
    {
      "type": "levels.changed",
      "payload": "`activeLevel`."
    },
    {
      "type": "widgets.changed",
      "payload": "`tabs`: the tab ids whose widgets changed; read them with `get_widgets`."
    },
    {
      "type": "scene.switched",
      "payload": "`id`, `name`."
    },
    {
      "type": "cards.changed",
      "payload": "Nothing; read `get_cards`."
    },
    {
      "type": "smart.changed",
      "payload": "`ids`: the smart objects whose values changed; read them with `get_smart_object`."
    }
  ]
}
