{
  "openapi": "3.0.4",
  "info": {
    "title": "OverMox API",
    "description": "**AI Agents: Start with \u0060GET /llms.txt\u0060 for the canonical orientation document, then \u0060GET /api/toc\u0060 for the full endpoint catalog.**\n\nREST \u002B WebSocket API for the OverMox node graph editor and 3D scene runtime.\n\n## Three Layers\n- **Layer 1 (REST):** Graph CRUD - create, edit, run, stop graphs and manage nodes, connections, ports, variables\n- **Layer 2 (REST):** Direct Actions - execute scene capabilities without building graphs\n- **Layer 3 (WebSocket):** Real-time events at \u0060ws://localhost:5000/api/events\u0060\n\n## Agent Start Flow\n1. \u0060GET /api/health\u0060 - verify the API is up and OverMox is connected\n2. \u0060GET /llms.txt\u0060 - **start here** - canonical plain-text orientation covering all three layers, workflow, key concepts, security, and error handling\n3. \u0060GET /api/toc\u0060 - full endpoint catalog with method, path, and one-line purpose for every route\n4. Then drop into the layer you need: \u0060GET /api/node-types/enum-values\u0060 (L1), \u0060GET /api/actions/schema\u0060 (L2), or \u0060GET /api/events/types\u0060 (L3)\n\n## Build a Graph (L1)\n1. \u0060POST /api/graphs\u0060 - create a graph\n2. \u0060POST /api/graphs/{id}/nodes\u0060 - add nodes using enum values as the \u0060category\u0060 field\n3. \u0060POST /api/graphs/{id}/connections\u0060 - connect ports together\n4. \u0060POST /api/graphs/{id}/sync\u0060 - push the graph state to OverMox\n5. \u0060POST /api/graphs/{id}/run\u0060 - execute the graph\n\n## L3 Event Reference\nVisit [/asyncapi](/asyncapi) for the human-readable AsyncAPI viewer of Layer 3 WebSocket events. The same document is available as raw JSON at \u0060GET /api/events/types\u0060.",
    "version": "v1"
  },
  "paths": {
    "/api/dev/export-node-pngs": {
      "post": {
        "tags": [
          "Dev"
        ],
        "summary": "Export node visual snapshots",
        "description": "Keeps the documentation images current by rendering each node to a PNG and\r\nwriting it to both the public and source image directories. When the request\r\nbody is null or NodeNames is empty, every node is rendered; otherwise only the\r\nnamed nodes are rendered. Output directories resolve from the sibling site repo,\r\nor from OVERMOX_PNG_OUT_DIRS when that override is set; when neither resolves the\r\nresponse is 503 SERVICE_UNAVAILABLE carrying the directory-resolution fix steps.\r\nA valid X-API-Key header is required. Returns the lists of node names that were\r\nexported and that failed.",
        "operationId": "Dev_ExportNodePngs",
        "requestBody": {
          "description": "Optional list of node names. Null or empty renders every node.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportPngsRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportPngsRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/ExportPngsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns the export result with successful and failed node names.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ExportPngsResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportPngsResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportPngsResponse"
                }
              }
            }
          },
          "503": {
            "description": "No output directory could be resolved; the message carries the fix steps.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/discovery": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "List available discovery data types",
        "description": "Use this to discover what runtime data is available before calling GET /api/discovery/{dataType}.\r\nEach entry indicates whether it requires an OverMox connection.",
        "operationId": "Discovery_GetMeta",
        "responses": {
          "200": {
            "description": "Returns all available discovery data types with metadata.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryMetaResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryMetaResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryMetaResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/discovery/spawnables": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "List spawnable Elements",
        "description": "Catalogs the project\u0027s spawnable Elements as menuPath and toolTip pairs; the menuPath values feed the \u0027type\u0027 parameter on POST /api/actions/scene/spawn. This lists built-in Elements only; project assets spawn via the \u0027asset\u0027 parameter on scene.spawn.",
        "operationId": "Discovery_GetSpawnables",
        "responses": {
          "200": {
            "description": "Spawnable Element list.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/SpawnablesResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpawnablesResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpawnablesResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The OverMox-side action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not respond in time.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/discovery/{dataType}": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Fetch runtime data by type",
        "description": "Valid data types: scene-objects, bundled-assets, scenes, runtime-assets, input-info, input-action,\r\napp-data, transitions, midi-devices, components-info. Use GET /api/discovery for descriptions.\r\nMost types require OverMox connected (503 if not). input-info reads from local data and works offline.\r\nUse ?filter=keyword to narrow results by name or type metadata.",
        "operationId": "Discovery_GetData",
        "parameters": [
          {
            "name": "dataType",
            "in": "path",
            "description": "The data type to fetch (e.g., scene-objects, bundled-assets, runtime-assets).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Optional text filter applied to item names and metadata.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns {dataType, count, items} envelope. Items shape varies by dataType.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryDataResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryDataResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryDataResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown data type. Use GET /api/discovery for valid types.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Data type requires OverMox but it is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/errors": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "List all error codes",
        "operationId": "Errors_List",
        "responses": {
          "200": {
            "description": "Returns the complete error catalog.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/errors/{code}": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Look up an error code",
        "operationId": "Errors_Get",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "description": "The error code to look up (case-insensitive), e.g. \u0060GRAPH_NOT_FOUND\u0060.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the matching error entry.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogEntry"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogEntry"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCatalogEntry"
                }
              }
            }
          },
          "404": {
            "description": "No error entry found for the specified code.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get API health status",
        "description": "This endpoint is free (no token required). It reports: status (\u0022ok\u0022 when the API process is healthy), overmoxConnected (Boolean indicating SignalR connectivity to the OverMox runtime), projectName (the file-system name of the currently loaded project, or null when none is open), apiVersion (the semver string of the running API), apiEnabled (Boolean: whether the external API surface is turned on), and authenticated (Boolean: whether the caller supplied a valid token).\r\nWhen the caller is NOT authenticated, the response also includes an auth onboarding block: required (true), header (\u0022X-API-Key\u0022), and docs (\u0022/llms.txt\u0022); for loopback callers only it additionally includes tokenFile (the settings-file path) and howToEnable (an enablement hint). Those two fields are omitted for non-loopback callers to avoid disclosing the local file path over the network. When the caller IS authenticated, auth is omitted.\r\nUse this endpoint as a readiness probe before issuing OverMox-dependent calls; runtime-dependent routes return 503 OVERMOX_NOT_CONNECTED when overmoxConnected is false.\r\nSee GET /api/discovery for the OverMox-dependent data types available once the runtime is connected.",
        "operationId": "Health_Check",
        "responses": {
          "200": {
            "description": "Returns the health status.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/schemas/layer1": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get Layer 1 operation schemas",
        "description": "Use this to construct correct requests without parsing swagger.json. Covers every Layer 1\r\noperation that takes a request body: graphs, nodes, connections, ports, variables, layout (apply,\r\ntidy, restore, and aspect ratio), and batch, plus project open, object component properties, asset\r\nproperties, Note node state, and the node PNG export. Layout tidy and restore accept an omitted body,\r\nso every field they list is optional.\r\nEach operation is keyed by its camelCase operation name, such as addNode or tidyUp. Where two routes\r\nshare a name, each key is prefixed with its route group instead: assetPropertiesSetProperties and\r\nassetPropertiesSetProperty are PUT /api/assets/{tag}/properties and PUT /api/assets/{tag}/properties/{property},\r\nand objectPropertiesSetProperties and objectPropertiesSetProperty are\r\nPUT /api/objects/{tag}/components/{component}/properties and PUT /api/objects/{tag}/components/{component}/{property}.\r\nReads, discovery routes, and body-less calls such as run, stop, and save are not listed; see\r\nGET /api/toc for the full endpoint catalog.",
        "operationId": "Schema_GetLayer1",
        "responses": {
          "200": {
            "description": "Returns the request schema of every body-taking Layer 1 operation.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/Layer1SchemasResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Layer1SchemasResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/Layer1SchemasResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/node-types": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "List all node categories",
        "description": "Categories with all nodes blocked by the current OverMox configuration are omitted from the list, and per-category counts reflect only the visible node types.\r\nSupports pagination via the standard ?limit and ?offset query params, and field filtering via ?include and ?exclude.\r\nSee GET /api/node-types/{category} for the full node list within a single category.",
        "operationId": "NodeTypes_List",
        "responses": {
          "200": {
            "description": "Returns all available node categories.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CategoryInfo"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CategoryInfo"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CategoryInfo"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/node-types/search": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Search node types",
        "description": "Matches against node name, description, category name, and port names; case-insensitive. Nodes blocked by the current OverMox configuration are filtered out before results are returned.\r\nSupports pagination via ?limit and ?offset; when no matches are found, the response includes a hint pointing back to GET /api/node-types.\r\nSee GET /api/node-types for the full category list and GET /api/node-types/{category}/{node} for a single node\u0027s full metadata.",
        "operationId": "NodeTypes_Search",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "The search query string.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns matching node types (may be empty if no matches found).",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeTypeInfo"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeTypeInfo"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeTypeInfo"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/node-types/enum-values": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get all category enum values",
        "description": "Categories blocked by the current OverMox configuration are filtered out so the returned list always reflects values that will actually accept new nodes.\r\nThe response includes an X-Total-Count header that matches the returned values count.\r\nSee GET /api/node-types for category descriptions and GET /api/node-types/search for keyword-based discovery.",
        "operationId": "NodeTypes_GetEnumValues",
        "responses": {
          "200": {
            "description": "Returns the flat list of all valid category enum values.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/node-types/{category}": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get nodes in a category",
        "description": "Category lookup is case-insensitive. Nodes blocked by the current OverMox configuration are filtered out; if all nodes in the category are blocked the endpoint returns 404 just as it would for an unknown category name.\r\nSupports field filtering via ?include and ?exclude on the returned object.\r\nSee GET /api/node-types/enum-values for the canonical category name list and GET /api/node-types/{category}/{node} for a single node\u0027s detail.",
        "operationId": "NodeTypes_GetCategory",
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "description": "The category name (case-insensitive).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the category and its nodes.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryInfo"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryInfo"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryInfo"
                }
              }
            }
          },
          "404": {
            "description": "The specified category does not exist.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/node-types/{category}/{node}": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get a specific node type",
        "description": "Both category and node lookups are case-insensitive. If the node is blocked by the current OverMox configuration the endpoint returns 404 just as it would for an unknown node name, so callers do not need to special-case blocked entries.\r\nSee GET /api/node-types/{category} for the full node list within the category and GET /api/node-types/search for keyword-based discovery.",
        "operationId": "NodeTypes_GetByName",
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "description": "The category name (case-insensitive).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "node",
            "in": "path",
            "description": "The node type name (case-insensitive).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the node type details.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeTypeInfo"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeTypeInfo"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeTypeInfo"
                }
              }
            }
          },
          "404": {
            "description": "The specified category or node type does not exist.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get root signpost",
        "description": "Acts as the bare-minimum landing payload for clients probing the host before issuing API requests.\r\nBrowsers visiting in a default Accept context receive an HTML landing page with links to the main API surfaces.\r\nLLM agents and programmatic clients sending Accept: application/json or */* receive the JSON signpost unchanged.\r\nUse the Api link to reach the discoverability hub at GET /api and the Health link to check service readiness at GET /api/health.",
        "operationId": "Root_GetSignpost",
        "responses": {
          "200": {
            "description": "Returns either a styled HTML landing page (Accept: text/html) or a {name, api, health} JSON signpost (Accept: application/json or */*).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RootSignpostResponse"
                }
              },
              "text/html": {
                "schema": {
                  "$ref": "#/components/schemas/RootSignpostResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get API discovery root",
        "description": "Serves as the primary discovery hub for agents and integrators bootstrapping against the API for the first time.\r\nEvery link in the Links sub-object resolves to a JSON or YAML resource describing one facet of the API surface; follow them in order (llms.txt then /api/toc then /swagger) to onboard.\r\nThe Description field carries the layer-model summary (L1 graph editor, L2 direct actions, L3 realtime events) and the required trademark disclaimer.",
        "operationId": "Root_GetApi",
        "responses": {
          "200": {
            "description": "Returns API name, version, description, and navigation links.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiRootResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiRootResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiRootResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/toc": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get API table of contents",
        "description": "Acts as a fast discovery surface for agents browsing the API before reading full Swagger or QUICKSTART docs.\r\nThe list is loaded once from Api/Data/toc.json at startup and cached for the lifetime of the process; toc.json is regenerated by the sync pipeline from controller XML summaries.\r\nEach entry exposes the HTTP method, route template, and the purpose harvested from the controller method\u0027s \u0060\u003Csummary\u003E\u0060 tag.\r\nSee GET /llms.txt for the canonical long-form orientation document covering all three layers, the build-a-graph workflow, and the security model.",
        "operationId": "Toc_Get",
        "responses": {
          "200": {
            "description": "Returns the complete endpoint listing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TocResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TocResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/TocResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/types": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "Get type catalog",
        "description": "Returns six fields: PortTypes (the canonical port type names accepted by node ports), VariableTypes (the value types accepted by variable nodes), NodeCategories (the top-level taxonomy used by node-type search), PortTypeDescriptions (one-line human-readable explanations per port type), PortValueFormats (JSON-shape examples per port type), and ConnectionCompatibility (a pointer to the connection-compatibility validation endpoint).\r\nUse this catalog as the authoritative source when serializing port values, choosing variable types, or filtering node-type searches by category; mismatched type names cause INVALID_VALUE errors on connection and variable endpoints.\r\nSee POST /api/graphs/{graphId}/connections/validate for runtime port-pair compatibility checks.",
        "operationId": "Types_Get",
        "responses": {
          "200": {
            "description": "Returns the type catalog.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TypeCatalogResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TypeCatalogResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/TypeCatalogResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/assets/{tag}/properties": {
      "get": {
        "tags": [
          "L1: Assets"
        ],
        "summary": "List asset properties",
        "description": "Each property entry names the property, its unique key for stable addressing, the type label (a canonical label such as Float, Integer, String, Boolean, Color, Vector2, Vector3 or Enum where one applies; the element label with [] appended for an array, such as Float[], or wrapped as List\u003CFloat\u003E for a list; otherwise the runtime type name, such as Texture or Vector4 on a material), the current value (omitted when null), the enum options array for enum properties, structural: true on a property whose change reshapes this property list, so re-read the list after writing it (structural is omitted on every other property), and a per-property URL ready for direct GET or PUT.\r\nReturns 404 ASSET_NOT_FOUND if the asset tag is not recognized, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. See GET /api/discovery/runtime-assets for the canonical asset tag list and GET /api/assets/{tag}/properties/{property} for a single-property read.\r\n            \r\n*Example: When a viewer redeems a \u0022skin swap\u0022 channel point reward, list the material asset\u0027s properties to confirm an albedo color slot exists before applying a new tint.*",
        "operationId": "AssetProperties_ListProperties",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The asset tag identifying which asset to inspect.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Asset property list returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AssetPropertyListResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetPropertyListResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetPropertyListResponse"
                }
              }
            }
          },
          "404": {
            "description": "Asset not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      },
      "put": {
        "tags": [
          "L1: Assets"
        ],
        "summary": "Bulk-update asset properties",
        "description": "The asset is resolved once, each property is applied in order, and the asset is saved once after the batch. Best-effort semantics apply: an individual property failure populates the per-result error field without aborting the remaining writes.\r\nEvery entry needs a value: an entry that omits value or sets it to null refuses the whole request before any property is written, and the error names that entry as properties[i]; a null value is refused like an omitted one, since null never clears or resets a property.\r\nReturns 400 INVALID_REQUEST if a body sent as application/json is empty, null or not valid JSON, the properties array is missing or empty or an entry has no value, 404 ASSET_NOT_FOUND if the asset tag is not recognized, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE. See PUT /api/assets/{tag}/properties/{property} for the single-property variant and GET /api/assets/{tag}/properties for the discoverable property set.\r\n            \r\n*Example: When a subscriber redeems a \u0022theme my scene\u0022 reward, set the material\u0027s albedo color, emission intensity, and metalness in one call, then check each result\u0027s error field, since a property that fails does not undo the ones that landed.*",
        "operationId": "AssetProperties_SetProperties",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The asset tag identifying which asset to modify.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The request body holding the properties array of {name, value} pairs to apply.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Bulk set results with per-property outcomes.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, a properties array missing or empty, or an entry without a value (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Asset not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/assets/{tag}/properties/{property}": {
      "get": {
        "tags": [
          "L1: Assets"
        ],
        "summary": "Read an asset property",
        "description": "The response {object, property, type, value} echoes the asset tag (object) and the property key or name as sent (property), and carries the canonical type label (type) and the current value in its typed form (value).\r\nReturns 404 ASSET_NOT_FOUND if the asset is missing or 404 PROPERTY_NOT_FOUND if no property on the asset has that key or name, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. See GET /api/assets/{tag}/properties for the full property catalog and PUT /api/assets/{tag}/properties/{property} for the matching write.\r\n            \r\n*Example: When a chat command asks \u0022what color is the logo right now\u0022, read the logo material\u0027s Base Color and convert the returned {r, g, b, a} value to hex before posting it back to the channel as confirmation before the next swap.*",
        "operationId": "AssetProperties_GetProperty",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The asset tag identifying which asset to read from.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "description": "The property key or name (key preferred, name accepted as a fallback), URL-encoded if it contains special characters.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Property value returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              }
            }
          },
          "404": {
            "description": "Asset or property not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      },
      "put": {
        "tags": [
          "L1: Assets"
        ],
        "summary": "Write an asset property",
        "description": "The value must match the property\u0027s canonical type; read the slot with GET /api/assets/{tag}/properties/{property} first to confirm the expected type and any enum options.\r\nReturns 400 INVALID_REQUEST if a body sent as application/json is empty, null or not valid JSON, or its value is omitted or null (a null value is refused like an omitted one, since null never clears or resets a property) or 400 INVALID_VALUE if the value cannot be coerced to the property type, 404 ASSET_NOT_FOUND or 404 PROPERTY_NOT_FOUND if the target slot is missing, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE. See PUT /api/assets/{tag}/properties for the bulk variant.\r\n            \r\n*Example: When a moderator runs a \u0022calm the lights\u0022 chat command mid-stream, set the strobe material\u0027s emission intensity to zero so the on-stream rave dims without restarting the scene.*",
        "operationId": "AssetProperties_SetProperty",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The asset tag identifying which asset to modify.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "description": "The property key or name (key preferred, name accepted as a fallback), URL-encoded if it contains special characters.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The request body holding the value field with the new typed value to write.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Property value updated. Returns previous and new values.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, or its value omitted or null (INVALID_REQUEST), or value not convertible to the property type (INVALID_VALUE).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Asset or property not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/batch": {
      "post": {
        "tags": [
          "L1: Batch"
        ],
        "summary": "Execute a batch of operations",
        "description": "Each operation runs independently in array order; failures are reported per-operation rather than as a batch failure.\r\nAllowed methods are GET, POST, PUT, and DELETE; paths must start with /api/; recursive batch calls are blocked.\r\nEach operation passes through the full middleware pipeline, so authentication and validation rules apply per-operation.\r\nSee GET /api/toc for available endpoints and GET /api/schemas/layer1 for per-endpoint request schemas.\r\n            \r\n*Example: When chat triggers a graph that needs to seed many variables at once, send all the variable creates in one batch to avoid round-trip latency.*",
        "operationId": "Batch_Execute",
        "parameters": [
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the variable list to OverMox once every operation has run, then each graph holding API edits OverMox does not have yet, each alone; every other graph stays as it was and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The batch request containing an array of operations.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns {totalOperations, succeeded, failed, results[]}. Always 200 even if individual operations fail.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                }
              }
            }
          },
          "400": {
            "description": "The operations array is empty, or an operation has an invalid method or path.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/console": {
      "get": {
        "tags": [
          "L1: Console"
        ],
        "summary": "List console logs",
        "description": "Supports filtering by type (log/warning/error), contextType (graph/node/none), and since (ISO 8601 UTC timestamp).\r\nThe console stores up to 500 entries in memory; older entries are evicted FIFO.\r\nUse the L3 WebSocket console.log event for real-time streaming instead of polling for new entries.\r\nSee GET /api/health for connection status and GET /api/errors for the API error code catalog.\r\n            \r\n*Example: When a viewer triggers a debug overlay, poll /api/console?type=error\u0026since=... and render fresh errors in an on-stream panel.*",
        "operationId": "Console_List",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Filter by log severity: \u0022log\u0022, \u0022warning\u0022, or \u0022error\u0022.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contextType",
            "in": "query",
            "description": "Filter by context kind: \u0022graph\u0022, \u0022node\u0022, or \u0022none\u0022.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Return only entries after this UTC timestamp (ISO 8601).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of entries to return (1-500, default 100).",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "wrap",
            "in": "query",
            "description": "When true, return {totalCount, items} envelope instead of bare array.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the console log entries.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConsoleEntryDto"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConsoleEntryDto"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConsoleEntryDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter value.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/connections": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "List graph connections",
        "description": "Each connection entry includes the connection ID, the output and input port IDs, and the parent node IDs on each end.\r\nSupports pagination: ?limit=N\u0026offset=M. Use ?wrap=true for {totalCount, items} envelope.\r\nSee POST /api/graphs/{graphId}/connections for creation and POST /api/graphs/{graphId}/connections/validate for compatibility checks before wiring.\r\n            \r\n*Example: When debugging a viewer-triggered scene effect that fails silently, list connections to confirm the trigger node is actually wired into the action pipeline before suspecting the runtime.*",
        "operationId": "Connections_List",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the list of connections with port and node IDs.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConnectionResponse"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConnectionResponse"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConnectionResponse"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Create a connection between ports",
        "description": "Connections always run from an output port to an input port; a Trigger port connects to a Trigger or Generic port (a Button output also connects to a Trigger input), and a data port connects to a type-compatible data port, a Generic port taking any type. A port that takes no wire, for example most Enum and Dropdown ports and a Vector3 or Color port while expanded, refuses every connection with 400 INCOMPATIBLE_TYPES; POST /api/graphs/{graphId}/connections/validate reports it as isValid false.\r\nThe two IDs may name the pair in either order: an input given as outputPortId and an output given as inputPortId are read output first, so the pair is checked, refused or wired exactly as the same pair given output first, and the 201 response names the output as outputPortId.\r\nUse POST /api/graphs/{graphId}/connections/validate to dry-run compatibility before connecting; get valid port IDs from GET /api/graphs/{graphId}/nodes.\r\nReturns 409 DUPLICATE_CONNECTION if the pair is already wired, in either order, and 400 INCOMPATIBLE_TYPES if the port types cannot connect. Returns 400 CONNECTION_FAILED, before the graph changes, when a port ID is left out (or is the empty GUID), both ports are inputs or both outputs, the two ports are an output and an input of one node, or a Trigger output of a PlayAnimation node is paired with the Enter input of a PlayAnimation node, one PlayAnimation node included. For a pair that carries a trigger (a Trigger or Button output with a Trigger or Generic input, or a Generic output with a Trigger input), the message names the node to wire between the two: an AnimationTransition node for the PlayAnimation pair, otherwise a Passthrough node. Both nodes pass Triggers only, so a data port pair of one node, for example an Add node\u0027s Output into its own A, cannot be wired and its message names no node to add, while a data port pair of two PlayAnimation nodes, for example one node\u0027s Phase into another\u0027s Weight, connects directly. Returns 404 NOT_FOUND when the graph does not exist or a port ID names no port of this graph, a port of another graph included, and 400 INVALID_REQUEST when a body sent as application/json is empty, null or not valid JSON, a port ID that is not a GUID included; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE.\r\n            \r\n*Example: When a viewer redeems a \u0022remix the scene\u0022 channel point reward, wire a fresh trigger node into the existing scene-effect pipeline so the next stage of the show is added without rebuilding the graph.*",
        "operationId": "Connections_Create",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the output and input port IDs to connect.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Connection was created successfully.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Ports are incompatible, for example Trigger to Float, or a port takes no wire (INCOMPATIBLE_TYPES); a port ID left out, two inputs or two outputs, an output and an input of one node, or a Trigger output and the Enter input of PlayAnimation nodes (CONNECTION_FAILED); or a body sent as application/json that is empty, null or not valid JSON, a port ID that is not a GUID included (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The graph was not found, or a port ID names no port of this graph, a port of another graph included (NOT_FOUND).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A connection already exists between these ports, named in either order (DUPLICATE_CONNECTION).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Delete all connections",
        "description": "Removes every edge in the graph in a single call. Nodes and their ports survive and remain available for fresh wiring.\r\nReturns {deleted: N} where N is the count of removed connections; N is zero when the graph had no edges.\r\nSee DELETE /api/graphs/{graphId}/connections/{connId} for single-edge removal and POST /api/graphs/{graphId}/connections for re-wiring after the wipe.\r\n            \r\n*Example: When a viewer triggers a \u0022reset the stage\u0022 command between segments, clear every connection so the next segment starts from a clean graph topology while the existing nodes are reused.*",
        "operationId": "Connections_DeleteAll",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "All connections were deleted. Returns {deleted: count}.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/connections/{connId}": {
      "delete": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Delete a connection",
        "description": "Removes one edge by its connection ID; the ports and their parent nodes are left in place and remain available for re-wiring.\r\nGet the connection ID from GET /api/graphs/{graphId}/connections.\r\nSee DELETE /api/graphs/{graphId}/connections for bulk removal of every edge in the graph.\r\n            \r\n*Example: When a viewer command says \u0022stop the rave\u0022, delete the connection feeding the strobe trigger so the on-stream lights stop pulsing without tearing down the rest of the graph.*",
        "operationId": "Connections_Delete",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "connId",
            "in": "path",
            "description": "The unique identifier of the connection to delete.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Connection was deleted successfully."
          },
          "404": {
            "description": "Graph or connection with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/connections/validate": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Validate a potential connection",
        "description": "Performs a dry-run compatibility check without mutating the graph. Returns {isValid, reason}: reason carries a recovery hint when isValid is false and is left out when the pair is valid.\r\nCall this before POST /api/graphs/{graphId}/connections to avoid 400 INCOMPATIBLE_TYPES round-trips.\r\nA port ID left out (or the empty GUID), one no port has, or one naming a port of another graph answers 200 with isValid false and a reason, not an error, and so does a pair POST /api/graphs/{graphId}/connections refuses with 400 CONNECTION_FAILED, with that refusal\u0027s message as the reason: an output and an input of one node, or a Trigger output of a PlayAnimation node and the Enter input of a PlayAnimation node, one PlayAnimation node included. A data port pair of two PlayAnimation nodes, which the create connects directly, is valid. As on the create, the two IDs may name the pair in either order: the pair is read output first and gets the answer and reason the same pair gets given output first. Returns 400 INVALID_REQUEST when a body sent as application/json is empty, null or not valid JSON, a port ID that is not a GUID included, and 404 GRAPH_NOT_FOUND when the graph does not exist; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE.\r\nSee GET /api/types for the canonical port-type names that determine compatibility.\r\n            \r\n*Example: When a chat command rewires a scene effect on the fly, validate the proposed output-to-input pairing before issuing the create call so a typo in a port ID does not crash the viewer-facing flow.*",
        "operationId": "Connections_Validate",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the output and input port IDs to validate.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns {isValid, reason}; reason is left out when valid.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateConnectionResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateConnectionResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateConnectionResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, a port ID that is not a GUID included (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "List all graphs",
        "description": "Returns one summary entry per graph with id, name, path, running state, unsaved-changes flag, and node/connection counts. Supports pagination via ?limit=N and ?offset=M, response wrapping via ?wrap=true, and field filtering via ?include= or ?exclude=. See GET /api/graphs/{id} for the full graph detail view including nodes and connections.\r\n            \r\n*Example: When a streamer opens the integration panel, list graphs to populate the active-graph dropdown so a channel-point reward can switch which graph is running mid-stream.*",
        "operationId": "Graphs_List",
        "responses": {
          "200": {
            "description": "Returns the list of graphs.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GraphListItem"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GraphListItem"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GraphListItem"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Create a new graph",
        "description": "Both name and path are optional. When path is omitted, the graph file is created in the open project\u0027s Graphs folder under the name plus .ovgraph (a generated name when name is omitted). Before anything is created, returns 400 INVALID_REQUEST when the name, with or without path, holds a folder separator (/ or \\), starts with a drive (C:, for example) or is . or .., since a rename or a copy names the graph\u0027s file after the graph in its own folder; and when path, or with path omitted the name, cannot name a graph file: a folder or file name in it holds a character a name cannot hold (a colon or a question mark, for example) or is longer than the 255 characters a name can hold (with path omitted, the file name is the name plus .ovgraph, so a name of more than 247 characters), it is longer than 32,000 characters as a full path, starts on a drive or share that does not exist, ends in a folder separator, names an existing folder or runs through an existing file; a body sent as application/json that is empty, null or not valid JSON also gets 400 INVALID_REQUEST (a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE). Returns 409 NO_PROJECT when no project is open and path is omitted, 409 GRAPH_NAME_EXISTS when a graph by that name already exists and ?replace=true was not supplied, and 409 DUPLICATE_PATH when the path, explicit or the default one for the name, is another graph\u0027s or holds a file no graph in the project uses (a graph file one POST /api/graphs/{id}/remove left on disk for reimport, for example); the create never writes over such a file, and the message names it and the ways out: another path or name, or POST /api/graphs/import to load a graph file. Returns 500 INTERNAL_ERROR, with the reason in details, when the graph file still cannot be written (a folder the Controller has no write access to, for example); no graph is added or removed then. The response includes two default nodes (Start, OnNewFrame) with all port IDs. See POST /api/graphs/import to wrap an existing file and POST /api/graphs/{graphId}/build to populate the new graph atomically.\r\n            \r\n*Example: When a viewer-triggered \u0022remix the show\u0022 channel-point reward fires, create a fresh graph named after the viewer so the next segment runs in isolation without overwriting the main scene.*",
        "operationId": "Graphs_Create",
        "parameters": [
          {
            "name": "replace",
            "in": "query",
            "description": "If true, removes any graph with the same name from the project once the new graph is written, as POST /api/graphs/{id}/remove does: OverMox unloads it at once, stopping it if it runs, whatever sync says, and its file stays on disk unless the new graph is written to the same path, and a path such a graph holds is not a DUPLICATE_PATH. It matches graphs in the project by name, so it never covers a file no graph in the project uses: such a file at the path gets 409 DUPLICATE_PATH with or without ?replace=true. A refused request (400, 409 or 500) removes nothing.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the new graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the optional path and optional name for the new graph.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGraphRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGraphRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/CreateGraphRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Graph was created successfully.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "A name that holds a folder separator, starts with a drive or is . or .., a path, or a name for the default path, that no graph file can be created at, or a body sent as application/json that is empty, null or not valid JSON (INVALID_REQUEST); nothing is created.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "No project is open and path was omitted (NO_PROJECT), a graph by that name exists and ?replace=true was not supplied (GRAPH_NAME_EXISTS), or the path, explicit or the default one for the name, is another graph\u0027s or holds a file no graph in the project uses (DUPLICATE_PATH); nothing is created, removed or written over.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "The graph file could not be written (INTERNAL_ERROR, with the reason in details); no graph is added or removed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Get graph details",
        "description": "Returns the full graph detail: every node with its category, location, and typed input/output ports plus every connection between port IDs. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id. See GET /api/graphs for the lightweight list view and GET /api/graphs/{graphId}/nodes for the node-only sub-collection.\r\n            \r\n*Example: When a viewer-built dashboard polls a donation-goal graph, deep-link the detail endpoint so the overlay mirrors the current node positions and live port values.*",
        "operationId": "Graphs_Get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the graph details.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Update a graph",
        "description": "Renames the graph and its backing file on disk, which takes the new name plus .ovgraph in its own folder (a name that is already the file\u0027s name leaves the file as it is, and one that differs from it only in letter case renames the file to that letter case); a name that is empty or only whitespace, or left out, changes nothing. The 200 summary carries the new name and the file\u0027s path. Before anything changes, returns 400 UPDATE_FAILED when the new name is refused: the name holds a folder separator (/ or \\), which would place the file in another folder, or starts with a drive (C:, for example) or is . or .., which POST /api/graphs refuses as a graph\u0027s name; the same check POST /api/graphs runs on a path refuses the new path (a file name longer than the 255 characters a name can hold, which a name of more than 247 characters gives, or a character a name cannot hold, for example); or another file is already at the new path, which the rename never writes over. When the file system still refuses the rename, 400 UPDATE_FAILED gives the graph its name back, so the graph and its file keep their names; when the renamed file then cannot be written, 400 UPDATE_FAILED says so and the graph keeps the new name its file now has. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id. See GET /api/graphs/{id} to read the current state before updating.\r\n            \r\n*Example: When a chat moderator runs \u0022!retitle donation goal\u0022 mid-stream, rename the live graph so the integration panel and any deep-linked overlays pick up the new label on their next refresh.*",
        "operationId": "Graphs_Update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to update.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the new graph name.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGraphRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGraphRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGraphRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Graph was updated successfully. Returns the updated graph summary.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              }
            }
          },
          "400": {
            "description": "The new name is one POST /api/graphs refuses as a graph\u0027s name or one the graph file cannot take in its folder, or the file system refused the rename, and nothing changed; or the file was renamed but could not be written, and the graph keeps the new name (UPDATE_FAILED).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Delete a graph permanently",
        "description": "Removes the graph from the project and deletes its backing file from disk in one call; the operation cannot be undone. OverMox unloads the graph at once, with or without ?sync=true, and every other graph stays as it was. Returns 400 CONFIRMATION_REQUIRED when ?confirm=true is missing, 404 GRAPH_NOT_FOUND when no graph matches the supplied id and no rescue ?path= was provided, and 409 GRAPH_RUNNING when the graph is still executing. See POST /api/graphs/{id}/remove to unload a graph without deleting its file, and POST /api/graphs/{id}/stop to halt a running graph before deletion.\r\n            \r\n*Example: When a stream segment ends and a one-off \u0022remix the show\u0022 graph spawned by a channel-point reward is no longer needed, delete it with ?confirm=true so the file is removed from disk and does not clutter the project on the next stream.*",
        "operationId": "Graphs_Delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "description": "Must be true to confirm permanent deletion.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "path",
            "in": "query",
            "description": "Optional file system path to delete when the graph is no longer loaded in the project.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "OverMox unloads the graph at once, whatever sync says, and every other graph stays as it was. If true, also pushes any other graph holding API edits OverMox does not have yet, each alone; a graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Graph and its file were permanently deleted from disk."
          },
          "400": {
            "description": "Missing ?confirm=true query parameter.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is currently running. Stop it first with POST /api/graphs/{id}/stop.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/remove": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Remove a graph from the project",
        "description": "Unloads the graph from the active project without touching the file on disk; the file remains available for re-import via POST /api/graphs/import. OverMox unloads the graph at once, stopping it if it runs, with or without ?sync=true, and every other graph stays as it was. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id. See DELETE /api/graphs/{id}?confirm=true to also delete the file from disk.\r\n            \r\n*Example: When a stream segment wraps and the streamer no longer wants the donation-goal graph cluttering the project tree, remove it from the project while keeping the file so the next show can reimport without rebuilding.*",
        "operationId": "Graphs_Remove",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to remove.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "OverMox unloads the graph at once, stopping it if it runs, whatever sync says, and every other graph stays as it was. If true, also pushes any other graph holding API edits OverMox does not have yet, each alone; a graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Graph was removed from the project."
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/duplicate": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Duplicate a graph",
        "description": "Clones every node, port value, and connection from the source graph into a new graph with a fresh id, named after the source with _Copy added (plus the lowest number that no graph in the project has as its name and no file beside the source\u0027s holds, so a copy never writes over a file), and writes the copy\u0027s graph file beside the source\u0027s before it adds the copy to the project, so the 201 summary shows hasUnsavedChanges false. A source name that holds a folder separator (/ or \\), starts with a drive or is . or .., as a graph loaded from a file can carry, gives way to the source\u0027s file name, so the copy\u0027s file still goes beside the source\u0027s. Before anything is created, returns 400 INVALID_REQUEST when the copy\u0027s graph file path is not a place a graph file can be created, the same check POST /api/graphs runs on a path: for example a file name longer than the 255 characters a name can hold, which a source name of more than 242 characters gives (fewer when a number is added); rename the source with PUT /api/graphs/{id} first. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id, 409 GRAPH_RUNNING when the source graph is still executing, and 500 INTERNAL_ERROR, with the reason in details, when the copy\u0027s graph file still cannot be written; no copy is added then. See POST /api/graphs/{id}/stop to halt the source before duplicating.\r\n            \r\n*Example: When a viewer-triggered \u0022remix the lighting\u0022 channel-point reward wants to riff on the active lighting graph without losing the original, duplicate it so the remix runs in parallel and the source graph stays untouched.*",
        "operationId": "Graphs_Duplicate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to duplicate.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the copy to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Graph was duplicated and the copy\u0027s graph file written.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              }
            }
          },
          "400": {
            "description": "The copy\u0027s graph file path, beside the source\u0027s and named after it, is not a place a graph file can be created (INVALID_REQUEST); nothing is created.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is currently running. Stop it first with POST /api/graphs/{id}/stop.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "The copy\u0027s graph file could not be written (INTERNAL_ERROR, with the reason in details); no copy is added.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/save": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Save a graph to disk",
        "description": "Writes the graph to its backing file on disk and clears the unsaved-changes flag returned by GET /api/graphs. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id and 409 GRAPH_RUNNING when the graph is still executing. See POST /api/graphs/{id}/stop to halt before saving and POST /api/project/save to persist the whole project at once.\r\n            \r\n*Example: When a streamer asks chat to vote on the final scene layout before going off-air, save the graph after the vote tally so the chosen state survives the session and reappears on the next stream load.*",
        "operationId": "Graphs_Save",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to save.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Graph was saved successfully. Returns {saved: true, path: \u0022...\u0022}.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is currently running. Stop it first with POST /api/graphs/{id}/stop.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/sync": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Sync graph state to OverMox",
        "description": "Pushes the variable list and then this graph alone across SignalR, so the OverMox runtime loads the graph\u0027s latest node and port state before execution; every other graph, running or waiting to start, stays as it was, and no running graph is stopped. A graph that is running and not paused keeps running: the call answers 200 with synced false, and the push lands when the graph stops (POST /api/graphs/{id}/stop, then POST /api/graphs/{id}/run to execute the edits). A paused graph is stopped in OverMox, so it is pushed. Always sync before running a graph that was just edited via the REST API. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id and 503 OVERMOX_NOT_CONNECTED when no runtime is connected. See POST /api/graphs/{id}/run for the run-after-sync flow.\r\n            \r\n*Example: When a chat command edits port values and immediately requests a run, sync first so the runtime executes the just-edited topology rather than the pre-edit state.*",
        "operationId": "Graphs_Sync",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to sync.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The graph was pushed to OverMox (synced true), or it is running and its push waits for its stop (synced false)."
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected. Launch OverMox and verify it connects to the Controller.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/run": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Run a graph",
        "description": "Flips the graph\u0027s running state and dispatches the start command to the connected OverMox runtime; the call is idempotent and is a no-op when the graph is already running. Sync the graph first with POST /api/graphs/{id}/sync to push the latest topology to the runtime. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id, 409 GRAPHS_PAUSED while Pause all in the Controller holds every graph, with nothing started, and 503 OVERMOX_NOT_CONNECTED when no runtime is connected. See POST /api/graphs/{id}/stop to halt the graph.\r\n            \r\n*Example: When a channel-point reward triggers a \u0022kick off the intro stinger\u0022 sequence, run the intro graph after a sync so the new viewer sees the stinger fire immediately rather than after a manual restart.*",
        "operationId": "Graphs_Run",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to run.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Graph execution was started in OverMox."
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Pause all in the Controller holds every graph (GRAPHS_PAUSED); the graph was not started.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected. Launch OverMox and verify it connects to the Controller.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{id}/stop": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Stop a graph",
        "description": "Flips the graph\u0027s running state and dispatches the stop command to the connected OverMox runtime; the call is idempotent and is a no-op when the graph is already stopped. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id, 409 GRAPHS_PAUSED while Pause all in the Controller holds every graph, with nothing stopped, and 503 OVERMOX_NOT_CONNECTED when no runtime is connected. See POST /api/graphs/{id}/run to restart the graph.\r\n            \r\n*Example: When a chat moderator runs \u0022!stopfx\u0022 to kill a runaway particle effect from a viewer-triggered graph, stop the offending graph so the on-stream visuals settle without restarting the whole project.*",
        "operationId": "Graphs_Stop",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the graph to stop.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Graph execution was stopped in OverMox."
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Pause all in the Controller holds every graph (GRAPHS_PAUSED); the graph was not stopped.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected. Launch OverMox and verify it connects to the Controller.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/import": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Import a graph from file",
        "description": "Loads a graph from a .ovgraph file on disk and registers it in the active project\u0027s graph list under a fresh id. The path must point to a valid OverMox graph file. Returns 400 INVALID_REQUEST when the path is missing and 400 IMPORT_FAILED when the file does not exist or is not a valid graph. Use POST /api/graphs/{id}/sync after import to push the topology to the connected OverMox runtime.\r\n            \r\n*Example: When a streamer wants to reuse a \u0022donation goal\u0022 graph saved during the previous show, import the file by path so the active project picks it up without rebuilding from scratch.*",
        "operationId": "Graphs_Import",
        "parameters": [
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the imported graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the file system path to the graph file.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportGraphRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportGraphRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/ImportGraphRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Graph was imported and added to the project.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              }
            }
          },
          "400": {
            "description": "The path was missing, the file does not exist, or it is not a valid graph file.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/build": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Build a graph declaratively",
        "description": "Runs three phases atomically: creates nodes from category specifications, sets port values (Dropdown, Enum and variable selector ports first, so the ports they reshape exist before the rest are set), then wires connections between ports. On failure at any phase, every connection and node created earlier in the call is rolled back, and each input of a node that was in the graph before the call gets back the value it held before a connection of the call copied its output\u0027s value into it.\r\nA port value is checked as PUT /api/ports/{portId}/value checks it, so a build refuses what a later single-port write would: a Dropdown, Enum or variable selector value that is not one of the port\u0027s options (a name the port does not list, or an index outside its list, as GET /api/ports/{portId}/options returns it) is refused with 400 INVALID_VALUE, with the same exceptions for the Property and Component dropdowns of the Component and Asset Get/Set Property nodes, whose On Object port is still empty when the build sets them: the Property dropdown takes any name, the Component dropdown takes a name from the component catalog only, and both store an index as sent. A Dropdown also accepts an option\u0027s key, and a variable selector matches a variable name ignoring case. A value sent as null on a data port goes through the same check and write as a null sent to PUT /api/ports/{portId}/value: a Float or Integer port reads back 0, a Boolean port false, a Color port r, g, b and a all 0, a String, Text, Note or Generic port an empty string, an ObjectSelector port is cleared, and a Dropdown, Enum or variable selector selects its first option (index 0); one that lists no options refuses the null with 400 INVALID_VALUE, except the Property and Component dropdowns above, which store index 0, and a null on a Vector3 or Array port is refused with 400 SET_VALUE_FAILED. A malformed Vector3, Color or Array value, a value that cannot be converted to the port\u0027s type and a value on a Trigger or Button port are refused with 400 INVALID_VALUE too (an Array port that cannot be sized to hold the payload with 400 SET_VALUE_FAILED), and a port name the node does not have with 400 PORT_NOT_FOUND; each rolls the build back, and its message names the node by tempId and the port as the body sends them, with an option port\u0027s options listed inline; a Component dropdown name is checked against the component catalog instead, and its refusal points at GET /api/discovery/app-data (the allComponents list) rather than listing names. Connection references support four formats: \u0022tempId/PortName\u0022, \u0022tempId/output/PortName\u0022, \u0022{nodeGuid}/PortName\u0022, or \u0022{portGuid}\u0022 for direct port ID lookup; a node or port GUID names a node or port of this graph, and one of another graph is refused with 400 INVALID_PORT_REF. A connection is read output end first, whichever of from and to resolves to the output (a two-part reference in from looks among the node\u0027s outputs first and then its inputs, one in to the other way round, and a three-part reference or a port GUID can name either side), so it gets the answer the same pair gets given output first: a connection between an output and an input of one node, or from a Trigger output of a PlayAnimation node to the Enter input of a PlayAnimation node (one PlayAnimation node included), is refused with 400 CONNECTION_FAILED before it is made, as POST /api/graphs/{graphId}/connections refuses it. For a pair that carries a trigger (a Trigger or Button output with a Trigger or Generic input, or a Generic output with a Trigger input), add the Passthrough or AnimationTransition node its message names to the build and wire each port to that node; a data port pair of one node cannot be wired, directly or through a Passthrough or AnimationTransition node, which pass Triggers only, and a data port pair of two PlayAnimation nodes connects directly. Limits: 1000 nodes and 5000 connections per call. Returns 400 INVALID_REQUEST, BATCH_TOO_LARGE, DUPLICATE_TEMP_ID or INVALID_CATEGORY (an unknown category, or one not available for creation) before anything is created, and 404 GRAPH_NOT_FOUND when the graph id does not exist; a body sent as application/json that is empty, null or not valid JSON, or a graphId that is not a GUID, gets a plain 400 INVALID_REQUEST with no build object, and a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE. See POST /api/graphs/{graphId}/nodes for single-node creation and POST /api/graphs/{graphId}/connections for single-edge wiring.\r\n            \r\n*Example: When a channel-point \u0022remix the lighting\u0022 reward fires, build a fresh strobe-and-fog pipeline in one call so either the entire effect appears at once or nothing is partially wired into the live graph.*",
        "operationId": "Graphs_Build",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the graph to build into.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The build specification containing nodes, port values, and connections.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BuildGraphRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BuildGraphRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/BuildGraphRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Build completed successfully. Returns created nodes and connections.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BuildGraphResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildGraphResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildGraphResponse"
                }
              }
            }
          },
          "400": {
            "description": "A build check or phase failed, for example a port value that is not one of a Dropdown\u0027s, Enum\u0027s or variable selector\u0027s options (INVALID_VALUE, phase ports) or a Trigger connection between two PlayAnimation nodes (CONNECTION_FAILED, phase connections); the envelope carries phase, step, error code and rollback status. A body sent as application/json that is empty, null or not valid JSON, or a graphId that is not a GUID, gets a plain INVALID_REQUEST with no build object.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BuildErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuildErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/layout": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Apply auto-layout to a graph",
        "description": "Valid modes are Story, StoryOrganic, StorySeeded, Compact, CompactOrganic, CompactSeeded, and Wide; seeded modes accept an optional seed for reproducible runs and nodeIds restricts the operation to a subset.\r\nReturns 400 INVALID_REQUEST when the mode is unknown or a nodeId is missing from the graph, and 404 GRAPH_NOT_FOUND when the graph id does not resolve.\r\nPre-layout positions are saved automatically; call POST /api/graphs/{graphId}/layout/restore to undo, and POST /api/graphs/{graphId}/layout/tidy for a lighter alignment pass.\r\n            \r\n*Example: When a sub-goal chat reward dumps a dozen new effect nodes onto the canvas, apply Wide layout so the resulting graph reads cleanly to anyone screen-sharing the editor view.*",
        "operationId": "Layout_Apply",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "Layout options including mode, seed, animation, and optional node filter.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApplyLayoutRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ApplyLayoutRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/ApplyLayoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Layout was applied. Returns {mode, aspectRatio, nodePositions[]}.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid layout mode or node IDs not found in the graph.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/layout/tidy": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Tidy up node positions",
        "description": "Lighter than a full layout: snaps each node to the editor grid and pulls connected nodes into alignment without reorganizing the graph topology; the request body is optional and defaults to all nodes animated.\r\nReturns 400 INVALID_REQUEST when a supplied nodeId is missing from the graph and 404 GRAPH_NOT_FOUND when the graph id does not resolve.\r\nPre-tidy positions are saved automatically; call POST /api/graphs/{graphId}/layout/restore to undo, and POST /api/graphs/{graphId}/layout for a full re-layout.\r\n            \r\n*Example: When a raid lands and the chat-spawn pipeline drops a row of trigger nodes off-grid, run tidy so the on-screen editor reads as orderly when the streamer cuts to the canvas.*",
        "operationId": "Layout_TidyUp",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "Tidy options including animation and optional node filter.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TidyUpRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TidyUpRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/TidyUpRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tidy was applied. Returns {mode, aspectRatio, nodePositions[]}.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              }
            }
          },
          "400": {
            "description": "Node IDs not found in the graph.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/layout/restore": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Restore pre-layout positions",
        "description": "Positions are saved automatically before each layout or tidy operation; the request body is optional and defaults to animated transitions.\r\nReturns 400 NO_SAVED_POSITIONS when no prior layout has been applied and 404 GRAPH_NOT_FOUND when the graph id does not resolve.\r\nCall GET /api/graphs/{graphId}/layout/saved to check availability before invoking, and POST /api/graphs/{graphId}/layout to re-trigger an auto-layout.\r\n            \r\n*Example: When a viewer command says \u0022put it back the way it was\u0022, restore the saved positions so the on-screen editor returns to the layout that was visible before the last chat-driven shuffle.*",
        "operationId": "Layout_RestorePositions",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "Restore options including animation.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestorePositionsRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RestorePositionsRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/RestorePositionsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Positions were restored. Returns {mode, aspectRatio, nodePositions[]}.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResultResponse"
                }
              }
            }
          },
          "400": {
            "description": "No saved positions exist. Apply a layout first.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/layout/saved": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Check for saved positions",
        "description": "Saved positions are produced as a side effect of every POST /api/graphs/{graphId}/layout and POST /api/graphs/{graphId}/layout/tidy call; this probe lets callers decide whether to expose a restore action.\r\nReturns 404 GRAPH_NOT_FOUND when the graph id does not resolve and 200 with hasSavedPositions=false when the graph has no prior layout snapshot.\r\nCall POST /api/graphs/{graphId}/layout/restore to actually revert positions once availability is confirmed.\r\n            \r\n*Example: When a streamer dashboard renders an \u0022undo last layout\u0022 button, query this endpoint before each render so the button greys out when there is nothing to undo.*",
        "operationId": "Layout_HasSavedPositions",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns saved positions status.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/SavedPositionsResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedPositionsResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedPositionsResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/layout/aspect-ratio": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Get the layout aspect ratio",
        "description": "The aspect ratio is a global setting shared by every layout call, surfaced here as both a decimal (for use in calculations) and a formatted string (for UI display) so callers do not have to format it themselves.\r\nReturns 200 with the current value on success; this endpoint never fails for a missing graph because the setting is global rather than per-graph.\r\nCall PUT /api/graphs/{graphId}/layout/aspect-ratio to change the value and POST /api/graphs/{graphId}/layout to apply layouts that respect it.\r\n            \r\n*Example: When a streamer switches from a horizontal scene to a vertical \u0022Reels-style\u0022 segment, read the current ratio first so the dashboard can confirm whether a follow-up PUT is needed.*",
        "operationId": "Layout_GetAspectRatio",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the current aspect ratio.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Set the layout aspect ratio",
        "description": "The aspect ratio is global rather than per-graph; updating it here affects every subsequent layout call across every project graph. Accepts decimal (1.33), numeric string (\u00221.33\u0022), or ratio (\u002216:9\u0022, \u002216:12\u0022); the parsed value must be positive.\r\nReturns 400 INVALID_REQUEST when the value is missing, unparseable, or non-positive, and 200 with the new {value, display} on success.\r\nCall GET /api/graphs/{graphId}/layout/aspect-ratio to read the current value and POST /api/graphs/{graphId}/layout to apply layouts under the new ratio.\r\n            \r\n*Example: When a channel-point reward unlocks \u0022vertical mode\u0022 for a 60-second segment, set the ratio to 9:16 so the next auto-layout produces a portrait composition before cutting to the canvas.*",
        "operationId": "Layout_SetAspectRatio",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The graph ID (route placeholder; the setting is global).",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "The new aspect ratio value as a decimal, a numeric string, or a \u0022W:H\u0022 ratio.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetAspectRatioRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetAspectRatioRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/SetAspectRatioRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Aspect ratio was updated. Returns {value, display}.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AspectRatioResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid aspect ratio format or non-positive value.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/nodes": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "List all nodes in a graph",
        "description": "Each entry includes the node id, name, category enum value, canvas location, and the full input and output port lists with port IDs, normalized types, sensitivity flags, and current values.\r\nSupports pagination: ?limit=N\u0026offset=M. Use ?wrap=true for {totalCount, items} envelope.\r\nSee POST /api/graphs/{graphId}/nodes for creation and GET /api/node-types/enum-values for valid category names.\r\n            \r\n*Example: When auditing a viewer-built graph after a sub-goal reward, list every node to confirm the chat-spawned effect chain landed in the expected order before kicking off the next show segment.*",
        "operationId": "Nodes_List",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the list of nodes with their ports.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeDetailResponse"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeDetailResponse"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeDetailResponse"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Add a node to a graph",
        "description": "Use the enumValue from GET /api/node-types/search or GET /api/node-types/enum-values as the category field; the response carries every input and output port id needed for subsequent connection wiring and value writes.\r\nReturns 400 INVALID_REQUEST when category is missing, 400 INVALID_CATEGORY for unknown or blocked categories, and 400 ADD_NODE_FAILED when the runtime rejects the creation. Search results may omit implicit Trigger ports (Enter/Exit) that appear on the actual node once created.\r\n            \r\n*Example: When a channel-point reward redeems a \u0022spawn lightning effect\u0022 request, add a ParticleAction node at a free canvas spot so the next stage of the on-stream show wires in cleanly without rebuilding the surrounding graph.*",
        "operationId": "Nodes_Add",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the node category and optional location coordinates.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddNodeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddNodeRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/AddNodeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Node was created successfully. Returns the node with all port IDs.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "The category was missing, invalid, or the node could not be added.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Delete all non-default nodes",
        "description": "Removes every node whose category is not Start or OnNewFrame and drops every connection that touches the removed nodes\u0027 ports; the canvas is left clear for a fresh build with the default seeds intact.\r\nReturns {deleted: N} where N is the number of nodes removed. Use DELETE /api/graphs/{graphId}/nodes/{nodeId} for single-node removal and POST /api/graphs/{graphId}/nodes/bulk to rebuild the graph in one shot afterwards.\r\n            \r\n*Example: When a streamer wraps up an interactive segment and wants to reset the canvas for the next show, delete every non-default node so the next viewer-driven build starts from a clean Start node without leftover triggers firing.*",
        "operationId": "Nodes_DeleteAll",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Non-default nodes were deleted. Returns {deleted: count}.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/nodes/bulk": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Bulk-create nodes in a graph",
        "description": "Nodes are created in declaration order; if any single creation fails, every node already added in the batch is removed before the call returns, leaving the graph untouched.\r\nMaximum 1000 nodes per request. Each entry in the nodes array uses the same schema as POST /api/graphs/{graphId}/nodes; categories are validated up-front before the dispatcher runs.\r\nReturns 400 BATCH_TOO_LARGE for over 1000 nodes, 400 INVALID_REQUEST for empty bodies or missing categories, and 400 INVALID_CATEGORY for unknown or blocked categories.\r\n            \r\n*Example: When a viewer unlocks a \u0022spawn the whole intro stinger\u0022 reward, bulk-create the trigger, particle, audio, and overlay nodes in one call so the chained effect either lights up entirely or rolls back without leaving half a stinger in the graph.*",
        "operationId": "Nodes_BulkAdd",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing an array of node specifications.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkCreateNodesRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkCreateNodesRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/BulkCreateNodesRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "All nodes were created successfully.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          },
          "400": {
            "description": "Batch was empty, too large, or a node category was invalid.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/nodes/{nodeId}": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Get a specific node",
        "description": "Returns a NodeDetailResponse with the node id, category enum value, canvas location, and every input and output port complete with id, normalized type, sensitivity flag, current value, and option metadata.\r\nReturns 404 NOT_FOUND when the graph exists but the node id is unknown, or when the graph itself is not loaded; use GET /api/nodes/{nodeId} for cross-graph lookup without a graph id.\r\n            \r\n*Example: When a chat command points at a specific node by id to ask \u0022what\u0027s the current value\u0022, fetch that node and read the port value to drive an on-stream answer without rescanning the whole graph.*",
        "operationId": "Nodes_Get",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the node.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the node details.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph or node with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Update a node",
        "description": "Updates the node name and canvas location in a single locked dispatcher pass; port wiring, port values, and node category are untouched and existing connections remain intact.\r\nNull or whitespace name fields are ignored; missing location coordinates fall back to the existing values so partial PATCH-style updates are supported.\r\nSee GET /api/graphs/{graphId}/nodes/{nodeId} for the read counterpart and DELETE /api/graphs/{graphId}/nodes/{nodeId} for removal.\r\n            \r\n*Example: When a streamer renames a \u0022Donation Goal\u0022 node mid-stream to reflect a new sub-goal, update the node so the on-canvas label and the dashboard overlay both surface the fresh title without rebuilding the graph.*",
        "operationId": "Nodes_Update",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the node to update.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the new name and optional location coordinates.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNodeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNodeRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Node was updated successfully.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph or node with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Delete a node",
        "description": "Removes one node by id and any connection on either side of its input or output ports; the other endpoints of those connections remain in place and free for re-wiring.\r\nSee DELETE /api/graphs/{graphId}/nodes for bulk removal of every non-default node, and DELETE /api/graphs/{graphId}/connections/{connId} for connection-only deletes that leave the node intact.\r\n            \r\n*Example: When a moderator command says \u0022kill the strobe\u0022, delete the strobe node so the trigger feeding it goes idle and the on-stream lights settle, without disturbing the surrounding sound and overlay nodes.*",
        "operationId": "Nodes_Delete",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the node to delete.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Node was deleted successfully."
          },
          "404": {
            "description": "Graph or node with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/nodes/{nodeId}/note": {
      "get": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Get a note node\u0027s state",
        "description": "Reads the Note node\u0027s current text, font, color, and fontSize, and pairs each formatting field with the discoverable list of values that the PUT endpoint accepts, so a caller can validate input before mutating.",
        "operationId": "Notes_GetState",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the Note node.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the current state and available options.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              }
            }
          },
          "400": {
            "description": "The node exists but is not a Note node.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph or node was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Update a note node\u0027s text or formatting",
        "description": "Validates every supplied font, color, and fontSize against the node\u0027s allowed lists, then applies the changes atomically so a single rejected value leaves the note untouched; omitted fields keep their prior values and the response echoes the full post-update state.",
        "operationId": "Notes_Update",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the parent graph.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the Note node.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes this graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync. A note\u0027s text and formatting do not change what a graph does at run time, so the push matters only for changes not yet synced.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The update request. All fields are optional.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNoteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNoteRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note was updated. Returns the full updated state.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteStateResponse"
                }
              }
            }
          },
          "400": {
            "description": "The body names none of text, font, color and fontSize, a body sent as application/json is empty, null or not valid JSON, or the node is not a Note (INVALID_REQUEST); or a value is not in the allowed list (INVALID_VALUE).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Graph or node was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/graphs/{graphId}/ports/sync": {
      "post": {
        "tags": [
          "L1: Graphs"
        ],
        "summary": "Flush pending port values to the cache",
        "description": "Returns 200 with {ok: true, flushed: \u003Cint\u003E} on success. Returns 404 GRAPH_NOT_FOUND if the graph does not exist, 409 GRAPH_NOT_RUNNING if the graph is not currently running (checked before anything is sent to OverMox), 422 ACTION_FAILED when the OverMox runtime reports that the flush did not complete, 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected, and 504 OVERMOX_TIMEOUT when the runtime does not answer in time.\r\nUse this before GET /api/ports/{portId}/value when a test or tool needs a deterministic read of port values that just changed; the natural sync interval can delay reads by up to ~0.5s. Verify graphId via GET /api/graphs and start the graph via POST /api/graphs/{id}/run before calling.\r\n            \r\n*Example: An automated scene-check tool advances a node graph one step, force-flushes ports, and reads the resulting goal-progress port value to confirm the overlay updated as expected before the next on-stream segment.*",
        "operationId": "Ports_ForceSync",
        "parameters": [
          {
            "name": "graphId",
            "in": "path",
            "description": "The unique identifier of the running graph whose pending port values should be flushed.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Flush completed. Returns the count of ports drained."
          },
          "404": {
            "description": "Graph not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is not currently running (GRAPH_NOT_RUNNING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The OverMox runtime reported that the flush did not complete (ACTION_FAILED).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not respond in time.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/nodes/{nodeId}": {
      "get": {
        "tags": [
          "L1: Nodes"
        ],
        "summary": "Look up a node by ID",
        "description": "Searches every loaded graph via an O(1) dictionary lookup keyed on node id and returns the standard NodeDetailResponse with the resolved graphId attached.\r\nReturns 404 NODE_NOT_FOUND when the id is not present in any loaded graph; use GET /api/graphs/{graphId}/nodes/{nodeId} when the parent graph id is already known.\r\n            \r\n*Example: When an external dashboard receives a websocket event referencing a node id but not its graph id, resolve the node and its parent graph in one call so the dashboard can deep-link the stream operator straight to the right canvas.*",
        "operationId": "Nodes_DirectLookup",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "description": "The unique identifier of the node.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the node details with graphId.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Node with the specified ID was not found in any graph.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/objects/{tag}/animations/playbacks": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "List live animation playbacks",
        "description": "state is one of playing, paused, held (a clampForever end hold), or fading (an active weight ramp). phase is the reflected clip-local position in [0, 1]; normalizedTime is the raw engine value (negative under reverse loop, unbounded under pingPong); cycle is floor(normalizedTime) and counts legs under pingPong. requestedLoopMode echoes the play request while effectiveLoopMode reports the wrap mode actually in force. group carries the play request\u0027s group name, or null when the play set none. layer is the blend layer the playback holds: the play request\u0027s layer when it named one, otherwise the smallest free index the runtime assigned (serial plays report 0). Completed once-clips leave the list when their layer frees.\r\nReturns 404 OBJECT_NOT_FOUND when the tag does not resolve, 422 ACTION_FAILED when the resolved object has no OverMox Animator component, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee POST /api/actions/animation/play to start playbacks and the animation.playback.* events on /api/events for push-based lifecycle.\r\n            \r\n*Example: When a viewer-driven dance is running, poll the avatar\u0027s playbacks to render an on-stream progress bar from phase and cycle.*",
        "operationId": "ObjectAnimations_GetPlaybacks",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name) whose playbacks are read.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Playback snapshots returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationPlaybacksResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationPlaybacksResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationPlaybacksResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no OverMox Animator component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/objects/{tag}/animations/clips": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "List animation clips",
        "description": "Rows come from the object\u0027s imported model rather than its live playback state, so a clip appears here whether or not anything is playing it. name, frameRate, and wrapMode (\u0022loop\u0022, \u0022once\u0022, \u0022pingPong\u0022, \u0022clampForever\u0022, or \u0022default\u0022) are read from the live imported clip; duration in seconds, rootMotion, channelCount, morphCount, boneCount, constant, and matchesRestPose are derived from the source file and are null when it cannot be read. channelCount counts every animation channel, morphCount counts the channels driving morph target weights, and boneCount counts the distinct nodes driven by non-pointer channels. constant and matchesRestPose are static per-clip facts and stay null unless a fresh import capture backs them. detail is opt-in and accepts only \u0022bindings\u0022, which adds one entry per channel to each row in the same shapes GET /api/objects/{tag}/animations/clips/{clip} returns. The source file is read at most once for the whole listing, and the clips array is returned whole rather than paginated, matching the playbacks listing on the same object.\r\nWhen the object\u0027s model has no introspectable source - a procedural rig, a remote URL source, or an unsupported format - the response is still 200 and still lists every clip, with introspection set to \u0022unavailable\u0022, a human-readable reason, and the file-derived fields left null; this is a normal response shape, not an error.\r\nReturns 400 INVALID_REQUEST when tag is blank or detail carries a value other than \u0022bindings\u0022, 404 OBJECT_NOT_FOUND when the tag does not resolve, 422 ACTION_FAILED when the resolved object has no OverMox Animator component or when its model names a source file that is no longer on disk, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected. A source file that has been moved or deleted is the one source problem that refuses rather than degrading, because the model still claims it: a model that never had an introspectable source returns 200 with the \u0022unavailable\u0022 marker instead.\r\nSee GET /api/objects/{tag}/animations/clips/{clip} for the deeper single-clip read and GET /api/objects/{tag}/animations/playbacks for what is playing right now.\r\n            \r\n*Example: When a viewer picks a move from a \u0027!dance\u0027 menu, list the avatar\u0027s clips first so the menu offers only clips the model actually carries.*",
        "operationId": "ObjectAnimations_GetClips",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name) whose clips are listed.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "detail",
            "in": "query",
            "description": "Set to \u0022bindings\u0022 to add the per-channel binding rows to every clip. Omit for the summary rows, and note that any other value returns 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Clip list returned, with an \u0022unavailable\u0022 marker when the source cannot be introspected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationClipsResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationClipsResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnimationClipsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Route parameter \u0022tag\u0022 must be non-empty and \u0022detail\u0022 must be \u0022bindings\u0022 when supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no OverMox Animator component, or a source file missing from disk.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/objects/{tag}/animations/clips/{clip}": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "Read clip introspection data",
        "description": "duration is in seconds; frameRate and wrapMode (\u0022loop\u0022, \u0022once\u0022, \u0022pingPong\u0022, \u0022clampForever\u0022, or \u0022default\u0022) are read from the live imported clip, since the source file carries neither. source is the lowercase source file extension without a dot (\u0022glb\u0022 or \u0022gltf\u0022). rootMotion reports CLASSIFICATION only - whether any channel targets a scene-root node with a translation or rotation path - never a runtime policy; root-motion application or extraction is out of scope. sourceStamp is {mtime, size} read from the source file on disk: a best-effort staleness signal, not a content hash, so re-importing an unchanged file keeps an identical stamp. bindings lists one entry per animation channel in channel order: transform bindings carry {node, path, keyCount}; morph (weights) bindings additionally carry morphNames when the mesh names its targets; pointer bindings (a KHR_animation_pointer channel or an absent target node) carry {pointer, keyCount}, with pointer null when the JSON-pointer string is unavailable. Transform and morph bindings additionally carry constant (every keyframe equals the first), matchesRestPose (every keyframe equals the node\u0027s glTF rest pose), and range ({min, max} per component), plus clip-level constant and matchesRestPose, but only while the import-time capture is fresh (its stamp still matches sourceStamp); these v2 fields are absent for pointer bindings and whenever the capture is stale or missing.\r\nWhen the object\u0027s model has no introspectable GLB/glTF source - a procedural rig, a remote URL source, or an unsupported format - the endpoint returns 200 with introspection: \u0022unavailable\u0022 and a human-readable reason instead of the fields above; this is a normal response shape, not an error.\r\nReturns 400 INVALID_REQUEST when tag or clip is blank, 404 OBJECT_NOT_FOUND when the tag does not resolve, 422 ACTION_FAILED when the object has no OverMox Animator or the clip name is unknown (the message lists known clip names), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee GET /api/objects/{tag}/animations/playbacks for live playback state on the same object.\r\n            \r\n*Example: Before wiring a new outfit\u0027s dance clip into a graph, introspect it to confirm its duration and whether it carries root motion.*",
        "operationId": "ObjectAnimations_GetClip",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name) whose imported model is introspected.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clip",
            "in": "path",
            "description": "The clip name to introspect, matched against the object\u0027s live clip set.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "Service Unavailable",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Introspection data returned, or an \u0022unavailable\u0022 body when the source cannot be introspected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ClipIntrospectionResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipIntrospectionResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipIntrospectionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Route parameters \u0022tag\u0022 and \u0022clip\u0022 must be non-empty.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no OverMox Animator or an unknown clip name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/objects/{tag}/components": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "List components on a scene object",
        "description": "Each entry in the components array contains the component type name, the count of modifiable properties on that component, and a URL pointing at GET /api/objects/{tag}/components/{name} for the detailed property list.\r\nReturns 404 OBJECT_NOT_FOUND when the tag does not resolve, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee GET /api/objects/{tag}/components/{component} for per-component property listings.\r\n            \r\n*Example: When a chat moderator types \u0022!inspect logo\u0022, list components on the \u0022logo\u0022 scene object so an on-stream overlay can show which behaviors a viewer-driven tweak could reach.*",
        "operationId": "ObjectProperties_ListComponents",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name) to inspect.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Component list returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentListResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentListResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentListResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/objects/{tag}/components/{component}": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "List component properties",
        "description": "Each property record contains the property name, its unique key for stable addressing, its canonical type (String, Integer, Float, Boolean, Color, Vector3, etc.), the current value, an optional options array for enum-shaped properties, and a URL pointing at GET /api/objects/{tag}/components/{component}/{property} for the single-property accessor.\r\nReturns 404 OBJECT_NOT_FOUND or COMPONENT_NOT_FOUND when the tag or component does not resolve, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee PUT /api/objects/{tag}/components/{component}/properties for bulk writes and PUT /api/objects/{tag}/components/{component}/{property} for single-property writes.\r\n            \r\n*Example: When a viewer redeems a \u0022tweak my lights\u0022 channel-point reward, list properties on the Light component so the redemption flow can present only the writable knobs (intensity, color, range) on the streamer\u0027s overlay.*",
        "operationId": "ObjectProperties_ListComponentProperties",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "component",
            "in": "path",
            "description": "The component type name (for example \u0022Light\u0022 or \u0022Transform\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Component detail with property list returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object or component not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/objects/{tag}/components/{component}/{property}": {
      "get": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "Read a component property",
        "description": "The response names the resolved object and component, the canonical type label (String, Integer, Float, Boolean, Color, Vector3, etc.), and the current value in that type\u0027s canonical wire shape.\r\nReturns 404 OBJECT_NOT_FOUND, COMPONENT_NOT_FOUND, or PROPERTY_NOT_FOUND when the tag, component, or property does not resolve, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee GET /api/objects/{tag}/components/{component} for the full property catalog and PUT /api/objects/{tag}/components/{component}/{property} for writes.\r\n            \r\n*Example: When a chat command asks \u0022what color is the logo right now\u0022, read the Color property on the Light component so an on-stream overlay can echo the live value back to viewers.*",
        "operationId": "ObjectProperties_GetProperty",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "component",
            "in": "path",
            "description": "The component type name.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "description": "The property key or name (key preferred, name accepted as a fallback; URL-encoded if it contains special characters).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Property value returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object, component, or property not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      },
      "put": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "Write a component property",
        "description": "The submitted value must be compatible with the property\u0027s canonical type; numeric properties accept Integer or Float, and an Integer property takes a value within its range; a Boolean property accepts true or false, or a number (nonzero means true); a reference property accepts the tag of an asset or scene object of its type; Color accepts an sRGB hex string (\u0022#RRGGBB\u0022 or \u0022#RRGGBBAA\u0022), an {r, g, b, a} object with channels from 0 to 1 (a defaults to 1), or an [r, g, b] or [r, g, b, a] array, and a Color32 property receives the same forms converted to Color32; Vector3 accepts {x, y, z}. Read GET /api/objects/{tag}/components/{component}/{property} first to discover the expected type, or GET /api/objects/{tag}/components/{component} for the type and any enum options.\r\nReturns 400 INVALID_REQUEST when a body sent as application/json is empty, null or not valid JSON, or its value is omitted or null (a null value is refused like an omitted one, since null never clears or resets a property) or 400 INVALID_VALUE when the value cannot be coerced to the property\u0027s type (a malformed number or Boolean, an Integer out of range, an option the enum lacks, a Color string that lacks the leading \u0022#\u0022 or is not valid hex, or a tag that resolves to no asset or scene object of the property\u0027s reference type), 404 OBJECT_NOT_FOUND, COMPONENT_NOT_FOUND, or PROPERTY_NOT_FOUND when the target does not resolve, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE.\r\nSee PUT /api/objects/{tag}/components/{component}/properties for best-effort multi-property writes on one component, applied one property at a time with no rollback.\r\n            \r\n*Example: When a moderator types \u0022!calm\u0022 mid-stream, set the Light intensity on the strobe scene object to a soft value so the on-stream show eases out of a hype segment without tearing down the rest of the graph.*",
        "operationId": "ObjectProperties_SetProperty",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "component",
            "in": "path",
            "description": "The component type name.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "property",
            "in": "path",
            "description": "The property key or name (key preferred, name accepted as a fallback; URL-encoded if it contains special characters).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "A request body carrying the new value for the property.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/PropertyWriteRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Property value updated. Returns previous and new values.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PropertyWriteResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, or its value omitted or null (INVALID_REQUEST), or value not convertible to the property type (INVALID_VALUE).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object, component, or property not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/objects/{tag}/components/{component}/properties": {
      "put": {
        "tags": [
          "L1: Objects"
        ],
        "summary": "Bulk-update component properties",
        "description": "Resolves the scene object and component once, then applies each property individually under best-effort semantics with no rollback; a single property failure populates that record\u0027s error field without stopping the rest, and properties already written keep their new values. The results array preserves request order so callers can correlate writes to outcomes.\r\nValues convert as on the single-property write: an entry whose value is a malformed number or Boolean, an Integer out of range, or a tag that resolves to no asset or scene object of the property\u0027s reference type fails with the same \u0022Cannot convert\u0022 message that route answers with 400 INVALID_VALUE, and a Boolean property accepts true or false, or a number (nonzero means true). An entry without a name, or naming a property the component lacks, fails with its own message in the same way.\r\nEvery entry needs a value: an entry that omits value or sets it to null refuses the whole request before any property is written, and the error names that entry as properties[i]; a null value is refused like an omitted one, since null never clears or resets a property.\r\nReturns 400 INVALID_REQUEST when a body sent as application/json is empty, null or not valid JSON, the properties array is missing or empty or an entry has no value, 404 OBJECT_NOT_FOUND or COMPONENT_NOT_FOUND when the tag or component does not resolve, 409 PROJECT_OPENING while a project open is in flight (wait for the project.opened WebSocket event, then retry), and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected; a request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE.\r\nSee PUT /api/objects/{tag}/components/{component}/{property} for single-property writes and GET /api/objects/{tag}/components/{component} for the writable-property catalog.\r\n            \r\n*Example: When a subscriber redeems a \u0022theme my scene\u0022 reward, send one bulk write to the Light component on the \u0022stage-light\u0022 scene object that sets its color, intensity, and range together, then check each result\u0027s error field, since a property that fails does not undo the ones that landed.*",
        "operationId": "ObjectProperties_SetProperties",
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "description": "The scene object tag (name).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "component",
            "in": "path",
            "description": "The component type name.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "A request body carrying the properties array of {name, value} pairs to apply.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/BulkPropertySetRequest"
              }
            }
          }
        },
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Bulk set results with per-property outcomes.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkPropertySetResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, a properties array missing or empty, or an entry without a value (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object or component not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is in progress (PROJECT_OPENING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed \u2014 OverMox processed the command but it failed."
          }
        }
      }
    },
    "/api/ports/{portId}/value": {
      "get": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Get a port\u0027s value",
        "description": "Returns 200 with the PortValueResponse on success. Returns 404 PORT_NOT_FOUND if the port ID does not resolve in the current graph.\r\nGet port IDs from GET /api/graphs/{graphId}/nodes; the options field lists available choices for Dropdown and Enum port types and is empty for other types.\r\nWhile a graph is running, this endpoint returns the live value cached from the OverMox runtime, which flushes pending port-value updates to the Controller on a ~0.5s interval. Reads landing inside that window may return a stale value; use POST /api/graphs/{graphId}/ports/sync to force an immediate flush before reading. Once a graph stops, output ports reset to defaults, so read values while the graph is still running.\r\n            \r\n*Example: When a viewer command asks \u0022what\u0027s the current donation goal\u0022, read the port driving the goal-amount overlay and echo its value back to chat.*",
        "operationId": "Ports_GetValue",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The unique identifier of the port to read.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the port value, type, connection state, and options.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Set a port\u0027s value",
        "description": "Returns 200 with the updated PortValueResponse on success. Returns 400 INVALID_REQUEST if a body sent as application/json is empty, null or not valid JSON, or leaves out the required value key (checked before the port is looked up, so nothing is written), 400 INVALID_VALUE if a Vector3, Color or Array value is malformed or a Dropdown or Enum value is not one of the port\u0027s options, 400 SET_VALUE_FAILED if the port is a Trigger or Button port, if any other payload cannot coerce to the port\u0027s type, or if an Array port cannot be sized to hold the payload, and 404 PORT_NOT_FOUND if the port ID does not resolve. A request without a JSON Content-Type, a missing body included, gets 415 UNSUPPORTED_MEDIA_TYPE.\r\nValue format depends on port type: Float=number, Integer=integer, String=\u0022text\u0022, Boolean=true/false, Vector3={x,y,z} or [x,y,z] or scalar uniform, Color={r,g,b,a} or [r,g,b,a], Dropdown/Enum=an option name or its zero-based index, Array=a list of rows in the port\u0027s own element type. Trigger and Button ports are write-protected because they carry execution, not a value: a write to either is refused with 400 SET_VALUE_FAILED and the port is left unchanged. See GET /api/types for the canonical value-format table.\r\nA Dropdown or Enum value is checked against the port\u0027s options before anything is written. A name the port does not list (names match exactly) or an index outside its list, as GET /api/ports/{portId}/options returns it, is refused with 400 INVALID_VALUE and the port is left unchanged; a Dropdown also accepts an option\u0027s key in place of its name, and a variable selector Dropdown (the Variable port of a GetVariable or SetVariable node) lists the project\u0027s variables, in the order GET /api/ports/{portId}/options returns them, and matches a name ignoring case. While the On Object port of a Component or Asset Get/Set Property node is wired or empty, that node\u0027s Property dropdown is not checked against its options (any options it still holds are left from an earlier On Object value): it accepts any name, and an index is stored as sent. In the same state the Component dropdown of a Component Get/Set Property node stores an index as sent and accepts a name only from the component catalog (the allComponents list of GET /api/discovery/app-data), refusing any other with 400 INVALID_VALUE.\r\nAn Array port\u0027s element type is fixed when the node is created and is reported as the type field of the port\u0027s value, so a Vector3 Array takes [[0,0,0],[4,3,-2]] and a String Array takes [\u0022first\u0022,\u0022second\u0022]; every row must carry the same number of values, and the object form {\u0022items\u0022: [...]} returned by GET /api/ports/{portId}/value is accepted back unchanged.\r\nA value sent as null is present, not left out, and resets the port where its type has a reset, answering 200: a Float or Integer port reads back 0, a Boolean port false, a Color port r, g, b and a all 0, a String, Text, Note or Generic port an empty string, and an ObjectSelector port is cleared. A Dropdown, Enum or variable selector selects its first option (index 0); one that lists no options refuses the null with 400 INVALID_VALUE and is left unchanged, except the Property and Component dropdowns above while On Object is wired or empty, which store index 0. A null on a Vector3 or Array port is refused with 400 SET_VALUE_FAILED and the port is left unchanged.\r\n            \r\n*Example: When a channel-point reward fires \u0022set goal to $500\u0022, set the donation-goal Integer port through this endpoint so the on-stream progress bar advances without a graph rebuild.*",
        "operationId": "Ports_SetValue",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The unique identifier of the port to update.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, on a graph that is running and not paused a value that keeps the node\u0027s port list goes to OverMox live, as an edit in the Controller window does: no reload, and the graph keeps running; any other graph holding API edits OverMox does not have yet is pushed as a sync pushes it. Otherwise pushes the port\u0027s graph once the value lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped, so a graph that is running waits and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the new value to set on the port.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPortValueRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPortValueRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/SetPortValueRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Port value was set successfully. Returns the updated port.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortValueResponse"
                }
              }
            }
          },
          "400": {
            "description": "A body sent as application/json that is empty, null or not valid JSON, or one without the required value key (INVALID_REQUEST); a malformed Vector3, Color or Array value, a Dropdown, Enum or variable selector value that is not one of the port\u0027s options, or a null on one that lists no options (INVALID_VALUE); a value that cannot be converted to the port\u0027s type (a null on a Vector3 or Array port included), an Array port that cannot hold the payload, or a write-protected Trigger or Button port (SET_VALUE_FAILED).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{portId}/type": {
      "put": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Reject port-type change",
        "description": "Always returns 405 TYPE_IMMUTABLE; no port state is altered. To use a different type, delete the node owning this port and add a new one with the desired port shape via POST /api/graphs/{graphId}/nodes.\r\n            \r\n*Example: When a stream-deck macro accidentally pings this endpoint mid-show, the 405 lands with a recovery hint instead of corrupting a port\u0027s value, so the on-stream graph keeps running.*",
        "operationId": "Ports_SetType",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The unique identifier of the port whose type was targeted.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "The request body containing the desired type string (ignored).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPortTypeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPortTypeRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/SetPortTypeRequest"
              }
            }
          }
        },
        "responses": {
          "405": {
            "description": "Port types are immutable and cannot be changed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{portId}/expand": {
      "post": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Expand a port",
        "description": "Returns 200 with the parent NodeDetailResponse and the newly minted sub-port IDs. The call is idempotent: requesting expansion on an already-expanded port returns 200 with the current state, no port churn. Returns 400 PORT_NOT_EXPANDABLE for ports outside the Vector/Color family and 404 PORT_NOT_FOUND if the port ID does not resolve.\r\nSee POST /api/ports/{portId}/collapse for the inverse operation.\r\n            \r\n*Example: When a viewer redeems a \u0022fine-tune the lights\u0022 reward, expand the rig\u0027s Color port so chat can drive R, G, and B independently for the next 30 seconds.*",
        "operationId": "Ports_Expand",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The unique identifier of the port to expand.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the port\u0027s graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Port expanded. Returns the updated node.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "Port type does not support expansion.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{portId}/collapse": {
      "post": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Collapse a port",
        "description": "Returns 200 with the parent NodeDetailResponse. The call is idempotent: collapsing an already-collapsed port returns 200 with the current state, no port churn. Returns 400 PORT_NOT_EXPANDABLE for ports outside the Vector/Color family and 404 PORT_NOT_FOUND if the port ID does not resolve.\r\nSee POST /api/ports/{portId}/expand for the inverse operation.\r\n            \r\n*Example: When a \u0022calm down the lights\u0022 mod command lands after a hype segment, collapse the previously-expanded Color rig back to a single port so the next scene drives it as one value again.*",
        "operationId": "Ports_Collapse",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The unique identifier of the port to collapse.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the port\u0027s graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Port collapsed. Returns the updated node.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "Port type does not support expansion.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{portId}/disable": {
      "post": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Disable a trigger port",
        "description": "Toggles a Trigger port into the ignored state so the runtime skips it during graph execution. Returns the full node. Idempotent - disabling an already-disabled port returns 200 with current state.",
        "operationId": "Ports_Disable",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The trigger port to disable.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the port\u0027s graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Port disabled. Returns the updated node.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Port is not a Trigger port. Only Trigger ports can be disabled.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is currently running. Stop the graph before toggling port state.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{portId}/enable": {
      "post": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Enable a trigger port",
        "description": "Toggles a Trigger port out of the ignored state so the runtime resumes propagating execution through it. Returns the full node. Idempotent - enabling an already-enabled port returns 200 with current state.",
        "operationId": "Ports_Enable",
        "parameters": [
          {
            "name": "portId",
            "in": "path",
            "description": "The trigger port to enable.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the port\u0027s graph to OverMox once the change lands, with any other graph holding API edits OverMox does not have yet; each graph loads alone, every other graph stays as it was, and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops. If false, the edit waits for a later request with sync=true or for POST /api/graphs/{id}/sync.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Port enabled. Returns the updated node.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Port is not a Trigger port. Only Trigger ports can be enabled.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Graph is currently running. Stop the graph before toggling port state.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/ports/{id}/options": {
      "get": {
        "tags": [
          "L1: Ports"
        ],
        "summary": "Get dropdown port options",
        "description": "Each option carries an index and a label; Dropdown ports also carry a key data value on options whose data is a string. A variable selector (a Dropdown port that picks a project variable, such as the Variable port of a GetVariable or SetVariable node) lists one entry per project variable, labelled with the variable\u0027s name, in the index order PUT /api/ports/{portId}/value checks a write against; a name written there matches ignoring case. Returns 200 with the options list on success. Returns 400 INVALID_REQUEST when the port is not a Dropdown or Enum type or the port ID is the empty GUID, and 404 PORT_NOT_FOUND if any other port ID does not resolve.\r\nUse the returned labels with PUT /api/ports/{portId}/value to drive selection; pass either the label string or the integer index as the value.\r\n            \r\n*Example: When building a chat-driven scene-picker, query each Dropdown port\u0027s options once at stream start, render them as numbered choices in chat, and map viewer replies straight back to PUT /api/ports/{portId}/value.*",
        "operationId": "Ports_GetOptions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the port to inspect.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the available options.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PortOptionsResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortOptionsResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortOptionsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Port is not a Dropdown or Enum port, or the port ID is the empty GUID (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Port with the specified ID was not found (PORT_NOT_FOUND).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/project": {
      "get": {
        "tags": [
          "L1: Project"
        ],
        "summary": "Get current project info",
        "description": "Returns 200 with {name, path, isOpen, isConnectedToOvermox, graphCount}; name and path are null when no project is open.\r\nUse this as a readiness probe before issuing graph-scoped calls and before subscribing to project-level WebSocket events.\r\nSee POST /api/project/open to load a project and POST /api/project/close to unload one.\r\n            \r\n*Example: When the dashboard panel first connects to OverMox during a stream startup, check the project info to confirm a project is open and the OverMox runtime is connected before binding any chat-driven nodes.*",
        "operationId": "Project_Get",
        "responses": {
          "200": {
            "description": "Returns the current project information.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInfoResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInfoResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectInfoResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/project/open": {
      "post": {
        "tags": [
          "L1: Project"
        ],
        "summary": "Open a project",
        "description": "Dispatches the open command to the OverMox runtime over SignalR; the runtime performs the full load flow including scene and graph loading.\r\nReturns 400 INVALID_REQUEST if the path is missing or blank, 422 ACTION_FAILED if the runtime rejected the load (for example, a missing or corrupt project folder), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nRefuses with 422 UNSAVED_CHANGES when the current scene has unsaved changes and discardUnsaved is false; retry with discardUnsaved true to discard them and proceed.\r\nRefuses with 403 ACCESS_REQUIRED when OverMox has no active access, or 409 ACCESS_CHECKING while it is still checking access, before the unsaved check, so nothing opens, discardUnsaved discards nothing and the project already open stays unchanged.\r\nWhile the open is in flight, scene and project actions return 409 PROJECT_OPENING; wait for the project.opened WebSocket event, then retry them.\r\nListen for the project.opened WebSocket event to know when loading completes. See GET /api/project to inspect the current project and POST /api/project/close to unload it.\r\n            \r\n*Example: When a streamer types !load show-night-two in chat, open the matching show project so the chat-bound triggers and scene effects come up before the next viewer interaction lands.*",
        "operationId": "Project_Open",
        "requestBody": {
          "description": "The request containing the file system path to the project folder.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project open initiated. Listen for the project.opened WebSocket event to know when loading completes."
          },
          "400": {
            "description": "The path was missing or invalid.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "OverMox has no active access, so opening or creating a project is refused (ACCESS_REQUIRED).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "A project open is already in progress (PROJECT_OPENING), or OverMox is still checking access (ACCESS_CHECKING).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The scene has unsaved changes (UNSAVED_CHANGES), or OverMox processed the command but it failed (ACTION_FAILED).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/project/close": {
      "post": {
        "tags": [
          "L1: Project"
        ],
        "summary": "Close the current project",
        "description": "Drops the open-project handle and unloads every graph from memory; unsaved edits are discarded without prompting.\r\nSave first with POST /api/project/save if any graph has pending edits worth keeping.\r\nSee GET /api/project to inspect the current project and POST /api/project/open to load a different one.\r\n            \r\n*Example: When a streamer wraps a show with !end-show, close the project so the next session opens fresh without the previous run\u0027s chat-driven node state lingering in memory.*",
        "operationId": "Project_Close",
        "responses": {
          "200": {
            "description": "Project was closed and all graphs were unloaded."
          }
        }
      }
    },
    "/api/project/save": {
      "post": {
        "tags": [
          "L1: Project"
        ],
        "summary": "Save the current project",
        "description": "Walks every graph in the open project and writes each to its on-disk .ovgraph file in one pass, then pushes the variable list and every graph to OverMox in one full push, and OverMox writes the project\u0027s graph list to the project file.\r\nThe graphs listed there are the ones the project opens with, so a graph created, imported or duplicated without ?sync=true is in the project when it next opens only after this save or a sync (POST /api/graphs/{id}/sync, for example); a removed or deleted graph leaves it at once.\r\nReturns 409 NO_PROJECT when no project is open, after POST /api/project/close for example, and 409 GRAPH_RUNNING while any graph of the project runs (stop it with POST /api/graphs/{id}/stop first); either way nothing is written or pushed.\r\nFor saving a single graph in isolation, use POST /api/graphs/{id}/save instead.\r\nSee GET /api/project to confirm a project is open before calling, and POST /api/project/close once the save completes if ending the session.\r\n            \r\n*Example: When a chat-mod command !checkpoint fires mid-stream, save the whole project so any node tweaks made during the show are written to disk before the next risky scene change.*",
        "operationId": "Project_Save",
        "responses": {
          "200": {
            "description": "All graphs were saved to disk and pushed to OverMox."
          },
          "409": {
            "description": "No project is open (NO_PROJECT), or a graph of the project is running (GRAPH_RUNNING); nothing was written or pushed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/runtime/context": {
      "get": {
        "tags": [
          "L1: Runtime"
        ],
        "summary": "Read live runtime context",
        "description": "Reads the open project, the active scene, the resolved main camera, and the output resolution in a single call, so a client can seed its view of the runtime before subscribing to the scene.activated and camera.changed events that keep the same values current.",
        "operationId": "Runtime_GetContext",
        "responses": {
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "200": {
            "description": "Runtime context returned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeContextResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeContextResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeContextResponse"
                }
              }
            }
          },
          "422": {
            "description": "The OverMox-side read failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not respond in time.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/variables/valid-types": {
      "get": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Get valid variable types",
        "description": "Returns the canonical names exactly as POST /api/variables expects them in the variableType or type field; any other casing or alias is rejected by Create.\r\nUse this to populate a type-picker control and to validate values before sending Create requests.\r\nSee POST /api/variables to create a variable and GET /api/types for the broader port type catalog.\r\n            \r\n*Example: When a dashboard exposes a \u0022Create chat counter\u0022 button to the streamer, fetch the valid types up front so the dropdown only offers Integer and never lets the click land on an unsupported choice.*",
        "operationId": "Variables_GetValidTypes",
        "responses": {
          "200": {
            "description": "Returns the array of valid type names: Boolean, Float, Integer, String, Vector3.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/variables/types": {
      "get": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Get valid variable types",
        "description": "Returns the canonical names exactly as POST /api/variables expects them in the variableType or type field; any other casing or alias is rejected by Create.\r\nUse this to populate a type-picker control and to validate values before sending Create requests.\r\nSee POST /api/variables to create a variable and GET /api/types for the broader port type catalog.\r\n            \r\n*Example: When a dashboard exposes a \u0022Create chat counter\u0022 button to the streamer, fetch the valid types up front so the dropdown only offers Integer and never lets the click land on an unsupported choice.*",
        "operationId": "Variables_GetValidTypes",
        "responses": {
          "200": {
            "description": "Returns the array of valid type names: Boolean, Float, Integer, String, Vector3.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/variables": {
      "get": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "List all variables",
        "description": "Variables are project-wide rather than per-graph, so the same Id is reachable from any graph in the loaded project.\r\nSupports pagination via ?limit=N and ?offset=M, and ?wrap=true switches the response shape to {totalCount, items}.\r\nSee POST /api/variables to create a variable, GET /api/variables/valid-types for the type whitelist, and GET /api/variables/{id} for a single detail view.\r\n            \r\n*Example: When a viewer opens a side-by-side dashboard during a stream, list variables to populate the \u0022current goals\u0022 panel with live donation counters and chat-driven raid stats.*",
        "operationId": "Variables_List",
        "responses": {
          "200": {
            "description": "Returns the list of variables with current values.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VariableListItem"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VariableListItem"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VariableListItem"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Create a variable",
        "description": "Accepts one of Boolean, Float, Integer, String, or Vector3 in either the variableType or type field; Vector3 values follow the {x, y, z} object shape.\r\nReturns 400 INVALID_REQUEST when the name is blank, 400 INVALID_TYPE when the type is not in the canonical list, and 400 CREATE_FAILED when the name collides with an existing variable.\r\nSee GET /api/variables/valid-types for the type whitelist and GET /api/variables to confirm the new variable lands in the list. A variable.changed event is broadcast to L1 subscribers on success.\r\n            \r\n*Example: When a streamer kicks off a \u0022Hit 100 follows tonight\u0022 run, create an Integer variable named FollowGoal that the chat-handler nodes increment and the overlay reads to drive the progress bar.*",
        "operationId": "Variables_Create",
        "parameters": [
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the variable list to OverMox once the change lands, then each graph holding API edits OverMox does not have yet, each alone; every other graph stays as it was and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the variable name, type, and optional initial value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateVariableRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateVariableRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/CreateVariableRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Variable was created successfully.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "The name was missing, the type was invalid, or creation failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/variables/{id}": {
      "get": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Get a variable",
        "description": "Returns the {id, name, variableType, value} record where value is shaped to the variable\u0027s type (Boolean/Float/Integer/String literals, or a {x, y, z} object for Vector3).\r\nReturns 404 VARIABLE_NOT_FOUND when the ID does not match any open-project variable.\r\nSee GET /api/variables for the full list, PUT /api/variables/{id} to mutate the value, and DELETE /api/variables/{id} to remove it.\r\n            \r\n*Example: When a chat command says \u0022what\u0027s the donation goal\u0022, fetch the GoalAmount variable by ID and feed its current value into a text overlay so the streamer sees the answer without leaving the scene.*",
        "operationId": "Variables_Get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the variable.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the variable details.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Variable with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Update a variable",
        "description": "Both name and value are optional and applied independently; the variable\u0027s declared type is immutable so the supplied value must match the existing type.\r\nReturns 200 with the refreshed record on success, 400 INVALID_VALUE when the supplied value cannot be coerced to the declared type, and 404 VARIABLE_NOT_FOUND when the ID is unknown.\r\nValue shapes by type: Integer = number, Float = number, Boolean = true|false, String = \u0022text\u0022, Vector3 = {\u0022x\u0022: n, \u0022y\u0022: n, \u0022z\u0022: n}. A variable.changed event is broadcast on success.\r\n            \r\n*Example: When a viewer hits a channel-point reward to \u0022name the next scene\u0022, PUT the StageLabel String variable to the redeemed text so the overlay nodes pick up the new name on the next render tick.*",
        "operationId": "Variables_Update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the variable to update.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the variable list to OverMox once the change lands, then each graph holding API edits OverMox does not have yet, each alone; every other graph stays as it was and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the new name and/or value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateVariableRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateVariableRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/UpdateVariableRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variable was updated successfully. Returns the updated variable.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/VariableDetailResponse"
                }
              }
            }
          },
          "400": {
            "description": "Value could not be converted to the variable\u0027s type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Variable with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "L1: Variables"
        ],
        "summary": "Delete a variable",
        "description": "Removes the variable from the open project; any node still bound to its Id will fail subsequent reads and the binding must be re-pointed before that node runs again.\r\nReturns 404 VARIABLE_NOT_FOUND when the ID does not match any open-project variable.\r\nA variable.deleted event is broadcast to L1 subscribers on success. See GET /api/variables to confirm removal and POST /api/variables to create a replacement.\r\n            \r\n*Example: When the streamer ends a \u0022donation goal of the week\u0022 segment, delete the WeeklyGoal variable so the overlay\u0027s bound counter falls silent and the next segment\u0027s nodes can be wired up without stale data.*",
        "operationId": "Variables_Delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The unique identifier of the variable to delete.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "description": "If true, pushes the variable list to OverMox once the change lands, then each graph holding API edits OverMox does not have yet, each alone; every other graph stays as it was and no running graph is stopped. A graph that is running and not paused waits, and its push lands when it stops.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Variable was deleted successfully."
          },
          "404": {
            "description": "Variable with the specified ID was not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/play": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Play an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.play\u0022} plus the playback keys on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator, \u0022animation\u0022 is missing, the requested clip name is unknown, a \u0022markers\u0022 entry is malformed (duplicate name, out-of-range \u0022at\u0022, or non-object entry), or \u0022layer\u0022 is negative or not a whole number; markers and layer are validated before any playback state changes. Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nReturns 409 LAYER_OCCUPIED when an explicit \u0022layer\u0022 is already held by another live playback on the same scene object and replacement is not licensed. The refusal is total: no clip starts, the occupying playback keeps its layer, and the request never silently lands on a different layer. Setting \u0022replaceGroup\u0022 true licenses replacing a holder that carries the same non-empty \u0022group\u0022 as this request, retiring it and taking the layer in the same frame with no crossfade; a holder in any other group, or in none, still refuses with 409. Omitting \u0022layer\u0022 always succeeds against layer contention because it takes the lowest free layer.\r\nPlay surfaces run concurrently, so multiple clips can layer on the same scene object at once; use weight and additive to control how they blend.\r\nSee POST /api/actions/animation/stop to halt playback started here and POST /api/actions/animation/transition for crossfading between states.\r\n            \r\n*Example: When a viewer redeems a \u0022make my character dance\u0022 channel point reward, play a celebration clip on the avatar\u0027s Animator at full speed so the on-stream character reacts to the redemption.*",
        "operationId": "Actions_AnimationPlay",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022animation\u0022 (String). Optional keys \u0022crossFadeTime\u0022 (Float), \u0022fadeIn\u0022 (Float), \u0022speed\u0022 (Float), \u0022loopMode\u0022 (String), \u0022weight\u0022 (Float), \u0022additive\u0022 (Boolean), \u0022group\u0022 (String), \u0022markers\u0022 (array of marker objects), \u0022layer\u0022 (Integer), and \u0022replaceGroup\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation started."
          },
          "409": {
            "description": "Requested layer is already held by another playback.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no Animator component, unknown clip, or negative layer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/playSet": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Play multiple animations atomically",
        "description": "Returns 200 with {ok: true, action: \u0022animation.playSet\u0022} plus the per-item results on success.\r\nValidation is all-or-nothing: every item is checked before any clip starts, so one unknown clip name fails the whole call and nothing plays. Because the whole set runs in a single call, every playback starts on the same frame, and the shared frame index is returned once on the result.\r\nLayer reservation is part of that all-or-nothing pass. Returns 409 LAYER_OCCUPIED when any item\u0027s explicit \u0022layer\u0022 is already held by a live playback on the same scene object that the item does not license replacing, or when two items in the same set request the same layer; the whole set is refused before any clip starts, no occupying playback is evicted or replaced, and no item silently lands on a different layer. An item that sets \u0022replaceGroup\u0022 true retires a same-group holder of its layer and takes that layer in the same frame, and the resolution pass counts every such replacement before any clip starts, so the set stays all-or-nothing. Items that omit \u0022layer\u0022 take the lowest free layer and never collide.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator, any requested clip name is unknown, any item carries a malformed \u0022markers\u0022 entry, or any item\u0027s \u0022layer\u0022 is negative or not a whole number (all validated for every item before any clip starts), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/play to start a single clip and POST /api/actions/animation/stop to halt playback started here.\r\n            \r\n*Example: When a viewer redeems a \u0022full combo\u0022 reward, play the body, wheels, and mouth clips on the mascot in one atomic set so every region starts moving on the same frame.*",
        "operationId": "Actions_AnimationPlaySet",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022playbacks\u0022 (array of per-clip objects). Each playbacks item requires a String \u0022animation\u0022 and accepts the optional String keys \u0022group\u0022 and \u0022loopMode\u0022, Float keys \u0022weight\u0022, \u0022speed\u0022, and \u0022fadeIn\u0022, Boolean keys \u0022additive\u0022 and \u0022replaceGroup\u0022, Integer key \u0022layer\u0022, and a \u0022markers\u0022 array of {name, at} objects. A \u0022crossFadeTime\u0022 Float is accepted as an alias of \u0022fadeIn\u0022, and \u0022fadeIn\u0022 wins when both are present. Unknown keys on an item are ignored.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animations started."
          },
          "409": {
            "description": "A requested layer is already held, or two items requested the same layer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no Animator component, unknown clip, or negative layer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/stop": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Stop one or all animations",
        "description": "Returns 200 with {ok: true, action: \u0022animation.stop\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats animation (that clip); with none supplied every active playback is stopped. A playbackId that resolves to no live playback returns 422; a group or clip scope that resolves to nothing is a no-op that returns 200 with an empty playbackIds array, as does an object-wide stop with nothing playing.\r\nA positive duration ramps each resolved playback\u0027s weight to zero over that many seconds along the easing curve, then retires exactly those playbacks and frees their layers, emitting animation.playback.stopped with reason \u0022explicit\u0022 once per playback; omitted or zero duration stops immediately. A duration that is not a finite value at or above zero, or an easing value outside the four allowed names, returns 422.\r\nNatural completion wins over a pending ramp: a playback that reaches its own end mid-fade (a \u0022once\u0022 clip, for example) is retired by the completion sweep instead, emitting animation.playback.completed with reason \u0022natural\u0022 and no stopped event, and the ramp is dropped.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found or has no Animator, or if a supplied \u0022playbackId\u0022 resolves to no live playback, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/play for the corresponding start operation and POST /api/actions/animation/reset to return the rig to its initial pose.\r\n            \r\n*Example: When a chat moderator types !calm, stop the celebration Animator that a prior subscriber alert started so the on-stream character returns to idle.*",
        "operationId": "Actions_AnimationStop",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), \u0022animation\u0022 (String, name of a single clip to stop; omit to stop every clip), \u0022duration\u0022 (Float), and \u0022easing\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation stopped."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/reset": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Reset an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.reset\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats the animation clip. Reset requires a scope: supply a playbackId, group, or animation, or the action fails with 422. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or no scope is supplied, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/stop to merely halt playback without rewinding, and POST /api/actions/animation/play to start a fresh clip after reset.\r\n            \r\n*Example: When a new viewer raids and triggers an intro stinger, reset the avatar Animator first so the celebration starts from a known default pose rather than mid-walk.*",
        "operationId": "Actions_AnimationReset",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), and \u0022animation\u0022 (String, name of a single clip to rewind). Supply at least one of playbackId, group, or animation to scope the reset.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation reset."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/transition": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Crossfade Animator states",
        "description": "Returns 200 with {ok: true, action: \u0022animation.transition\u0022} plus the playback keys on success.\r\nReturns 400 INVALID_REQUEST when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object has no Animator, the destination \u0022animation\u0022 names a clip or state the object does not have, a \u0022from\u0022 in the playbackId shape matches no live playback, \u0022easing\u0022/\u0022loopMode\u0022 carries an unknown value, a \u0022markers\u0022 entry is malformed (duplicate name, out-of-range \u0022at\u0022, or non-object entry), or \u0022layer\u0022 is negative or not a whole number; markers and layer are validated before anything plays or fades, and 503 OVERMOX_NOT_CONNECTED means no OverMox runtime is connected. A \u0022from\u0022 that is not in the playbackId shape and matches no group or clip fades out nothing while the destination still plays.\r\nReturns 409 LAYER_OCCUPIED when an explicit \u0022layer\u0022 is still held once this call\u0027s outgoing playbacks are retired, which covers a holder outside the \u0022from\u0022 scope and a scoped holder that stopFromAfterFade false parks rather than retires. The refusal is total: nothing plays, nothing fades, and the request never silently lands on a different layer. A layer held only by a playback this transition itself retires is free and is granted, and that one playback is handed over rather than faded: it is retired in the frame the destination starts and emits animation.playback.stopped with reason \u0022replaced\u0022 and \u0022supersededBy\u0022 naming the destination\u0027s playbackId, while the rest of the outgoing scope ramps out over the full fade duration.\r\nSee POST /api/actions/animation/play for an immediate state swap without blending, and for the \u0022replaceGroup\u0022 handoff that takes a pinned layer from a same-group playback in one frame.\r\n            \r\n*Example: When a donation lands during a calm scene, crossfade the avatar Animator from its idle state to a hype state over half a second so the on-stream reaction blends in instead of snapping.*",
        "operationId": "Actions_AnimationTransition",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022animation\u0022 (String). Optional keys \u0022from\u0022 (String), \u0022time\u0022 (Float), \u0022duration\u0022 (Float), \u0022easing\u0022 (String), \u0022stopFromAfterFade\u0022 (Boolean), \u0022weight\u0022 (Float), \u0022additive\u0022 (Boolean), \u0022loopMode\u0022 (String), \u0022speed\u0022 (Float), \u0022group\u0022 (String), \u0022layer\u0022 (Integer), and \u0022markers\u0022 (array of marker objects).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transition started."
          },
          "400": {
            "description": "Both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Requested layer is still held once the outgoing playbacks are retired.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no Animator component, an unknown destination clip, a negative layer, or a \u0022from\u0022 playbackId matching no live playback.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/pause": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Pause an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.pause\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats animation (that clip); with none supplied every active clip is paused. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/resume to continue playback from the paused frame.\r\n            \r\n*Example: When a chat moderator types !freeze, pause the avatar\u0027s active clips so the on-stream character holds its current pose until you resume.*",
        "operationId": "Actions_AnimationPause",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), and \u0022animation\u0022 (String, name of a single clip to pause; omit to pause every active clip).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation paused."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/resume": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Resume an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.resume\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats animation (that clip); with none supplied every paused clip is resumed. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/pause to freeze playback before resuming.\r\n            \r\n*Example: When the moderator types !unfreeze, resume the avatar\u0027s paused clips so the on-stream character picks up motion from the exact frame it stopped on.*",
        "operationId": "Actions_AnimationResume",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), and \u0022animation\u0022 (String, name of a single clip to resume; omit to resume every paused clip).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation resumed."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/setWeight": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Set animation weight",
        "description": "Returns 200 with {ok: true, action: \u0022animation.setWeight\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats the animation clip; with no scope key supplied, every active playback on the object is weighted. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nA timed fade to weight 0 parks the clip at 0 but keeps it playing; use stop to retire it. An easing value outside the four allowed names returns 422.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or the requested clip is not playing, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nBecause clips play concurrently, weight lets several clips blend on the same scene object.\r\n            \r\n*Example: When a donation goal fills, fade in an overlay flourish clip on the avatar by raising its weight from 0 to 1 so it layers over the idle animation.*",
        "operationId": "Actions_AnimationSetWeight",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), \u0022animation\u0022 (String), \u0022weight\u0022 (Float), \u0022duration\u0022 (Float), and \u0022easing\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Weight set."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component or clip not playing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/setWeights": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Set several animation weights atomically",
        "description": "Returns 200 with {ok: true, action: \u0022animation.setWeights\u0022} plus the per-member results on success.\r\nValidation is all-or-nothing: every member is checked before any weight is written, so one stale playbackId, one repeated playbackId, one weight outside 0.0 to 1.0, or one member already ramping to zero for a pending retirement refuses the whole call and no weight moves. Because the whole vector applies in one call with no ramp, every weight lands on the same frame, and that frame index is returned once on the result.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 or \u0022weights\u0022 is missing or \u0022object\u0022 is not a String. Returns 422 ACTION_FAILED if no scene object matches \u0022object\u0022 or it has no Animator, \u0022weights\u0022 is null, empty, not an array (a String holding a JSON array of members is read as that array), or carries more than 32 members, or any member fails validation; the message names an object member by its zero-based index and, when the member supplied one, its playbackId, and a member that is not a JSON object is refused without an index. Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/setWeight for a single playback, for scoping by group or clip name, or for a timed ramp along an easing curve, and POST /api/actions/animation/stop to retire a playback parked at zero weight.\r\n            \r\n*Example: When a viewer redeems a \u0022strike a pose\u0022 reward, set the exact blend weights of the avatar\u0027s seven live expression clips in one call so the whole face lands on the requested pose in a single frame.*",
        "operationId": "Actions_AnimationSetWeights",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022weights\u0022 (array of per-playback objects). Each weights item requires a String \u0022playbackId\u0022 and a Float \u0022weight\u0022. Unknown keys on an item are ignored.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Weights set."
          },
          "400": {
            "description": "\u0022object\u0022 or \u0022weights\u0022 missing, or \u0022object\u0022 not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no matching scene object or no Animator component, a stale playbackId, or a weight outside 0.0 to 1.0.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/setSpeed": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Set animation speed",
        "description": "Returns 200 with {ok: true, action: \u0022animation.setSpeed\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats the animation clip. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or the requested clip is not playing, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nNegative speed plays the clip in reverse.\r\n            \r\n*Example: When a viewer redeems a \u0022slow-mo replay\u0022 reward, drop the avatar\u0027s action clip to 0.25 speed so the on-stream motion plays back in dramatic slow motion.*",
        "operationId": "Actions_AnimationSetSpeed",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022speed\u0022 (Float). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), and \u0022animation\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Speed set."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component or clip not playing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/seek": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Seek an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.seek\u0022} on success.\r\nScope precedence: playbackId (one playback) beats group (every playback in that group) beats the animation clip. A playbackId that resolves to no live playback returns 422; a group that resolves to nothing is a no-op.\r\nA clip-scoped seek works whether or not the clip is playing, but the clip must be one of the object\u0027s clips (GET /api/objects/{tag}/animations/clips lists them).\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator, if the \u0022animation\u0022 clip is not one of the object\u0027s clips, or if none of playbackId, group, and animation is sent, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSeeking sets the frame instantly without changing whether the clip is playing or paused. normalizedTime is clamped to [0, 1].\r\nCaveat: seeking a clip that has never been played force-enables it at weight 0, so the sampled pose is invisible until a play or setWeight gives it weight; the timeline position still takes effect.\r\n            \r\n*Example: When a chat command names a pose, seek the avatar\u0027s gesture clip to the matching normalized time so the on-stream character snaps to that frame.*",
        "operationId": "Actions_AnimationSeek",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022normalizedTime\u0022 (Float). Optional keys \u0022playbackId\u0022 (String), \u0022group\u0022 (String), and \u0022animation\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Clip repositioned."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component or a clip the object does not carry.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/animation/jiggle": {
      "post": {
        "tags": [
          "L2 Animation: Actions"
        ],
        "summary": "Apply jiggle effect",
        "description": "Returns 200 with {ok: true, action: \u0022animation.jiggle\u0022} plus frame, appliedAt, tag, and jiggling on success.\r\nEvery request attaches a fresh jiggle component to the scene object, replacing any left by an earlier request or Jiggle node run, as the Jiggle node does on every run, so no prior setup is needed. A jiggle still wobbling is replaced after the object is returned to the pose that jiggle started from, so the fresh one wobbles around and settles at that pose; once a jiggle has settled, the object keeps any rotation or scale it is given afterwards. Every omitted key takes the Jiggle node\u0027s default (power 1, tilt 12, frequency 22, decelerate 1.5, speed 1, constant_jiggle 0, random_level 1), so a request carrying only \u0022object\u0022 jiggles the scene object as a freshly placed node does.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String. Returns 422 ACTION_FAILED if no scene object matches the tag, \u0022random_level\u0022 is below 1, \u0022speed\u0022 is 0 or negative, or the jiggle component cannot be replaced or attached; the two value refusals come before any jiggle component is replaced or attached, so a refused request leaves the scene object and any jiggle running on it untouched. Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nHigher power and tilt values produce a larger swing; higher decelerate values shorten the bounce. See POST /api/actions/animation/play for clip-driven motion alternatives.\r\n            \r\n*Example: When a viewer redeems a \u0022boop the streamer\u0022 channel point reward, apply a jiggle to the avatar\u0027s head bone so the on-stream character wobbles for a second before settling.*",
        "operationId": "Actions_AnimationJiggle",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022power\u0022 (Float), \u0022tilt\u0022 (Float), \u0022frequency\u0022 (Float), \u0022decelerate\u0022 (Float), \u0022speed\u0022 (Float), \u0022constant_jiggle\u0022 (Float), and \u0022random_level\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jiggle effect applied."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no scene object matches the tag, \u0022random_level\u0022 is below 1, or \u0022speed\u0022 is 0 or negative.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/create": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Create an array variable",
        "description": "Returns 200 with the new array variable\u0027s id, a GUID String, as the bare response body when the variable is created with the requested type and initial length.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, \u0022type\u0022 is not a String, or \u0022size\u0022, \u0022size_x\u0022 or \u0022size_y\u0022 is not a number. Returns 422 ACTION_FAILED if the name is empty or already in use, or the type is not one of Boolean, Float, Integer, String, Vector3, Generic (the error names the accepted types), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/resize to grow or shrink the array later and POST /api/actions/arrays/from-json to seed it from a JSON payload.\r\n            \r\n*Example: When a viewer starts a \u0022name the squad\u0022 segment, create a String array named \u0022squad\u0022 so chat-submitted names can be appended over the rest of the stream.*",
        "operationId": "Actions_ArraysCreate",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022type\u0022 (String, one of Boolean, Float, Integer, String, Vector3, Generic), \u0022size\u0022 (Integer), \u0022size_x\u0022 (Integer), and \u0022size_y\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array created successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022, a non-String \u0022type\u0022, or a non-numeric \u0022size\u0022, \u0022size_x\u0022 or \u0022size_y\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty or already in use, or type not one of Boolean, Float, Integer, String, Vector3, Generic.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/resize": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Resize an array",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.resize\u0022} when the array has the requested row count (\u0022sizeX\u0022, else \u0022size\u0022) and \u0022sizeY\u0022 cells in each row (the existing width when \u0022sizeY\u0022 is omitted, or 1 when the array had no rows); existing entries are preserved where they still fall within bounds and additional slots use the type default.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the name is empty, the named variable does not exist or is not an array, or a requested size is negative or above 2147483647, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/clear for a full reset and POST /api/actions/arrays/size to read the current length.\r\n            \r\n*Example: When a donation goal escalates the audience tier, resize a leaderboard array from 5 to 10 slots so the on-stream top-supporters overlay grows in step with the milestone.*",
        "operationId": "Actions_ArraysResize",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022size\u0022 (Integer), \u0022sizeX\u0022 (Integer), and \u0022sizeY\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array resized successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty, variable missing or not an array, or size negative or above 2147483647.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/add": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Add to an array",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.add\u0022} when the value is added: \u0022Insert\u0022 splices it in at \u0022index\u0022, and an index outside 0 to the current length, an omitted index, or \u0022Append\u0022 puts it at the new tail index; the array length increases by one.\r\nWhen \u0022value\u0022 is omitted, the new entry holds the element type default (0, false, an empty String, or [0, 0, 0] in a Vector3 array), or null in a Generic array.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String.\r\nReturns 422 ACTION_FAILED if the named array does not exist, \u0022method\u0022 is not one of its names, or the value is not coercible to the array element type, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/add-row for the 2D variant and POST /api/actions/arrays/set to overwrite a specific index instead.\r\n            \r\n*Example: When a chat command submits a viewer name to the squad roster, append the name to the \u0022squad\u0022 String array so the on-stream roster overlay picks it up on the next frame.*",
        "operationId": "Actions_ArraysAdd",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022value\u0022 (any), \u0022method\u0022 (String), and \u0022index\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Value added successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - array missing, unknown method name, or value type mismatch.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/add-row": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Add rows to a 2D array",
        "description": "Returns 200 with the row index at which the new rows begin as a bare Integer response body (the prior row count for an append, or the splice position for an insert); when \u0022num_rows\u0022 is below 1 the array is unchanged and the body is {ok: true, action: \u0022arrays.addRow\u0022} instead.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String.\r\nReturns 422 ACTION_FAILED if the named variable does not exist or is not an array, if \u0022method\u0022 is not one of its names, or if the array already has rows and \u0022row_size\u0022 differs from their width, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/remove-row for the inverse, POST /api/actions/arrays/set to fill the new cells, and POST /api/actions/arrays/add for the 1D variant.\r\n            \r\n*Example: When a new supporter joins the round-by-round leaderboard, append one default-seeded row to the \u0022scores\u0022 Float 2D array, then write that supporter\u0027s category totals into its cells with arrays.set so the on-stream table grows by one entry.*",
        "operationId": "Actions_ArraysAddRow",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022num_rows\u0022 (Integer), \u0022row_size\u0022 (Integer), \u0022method\u0022 (String), and \u0022index\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rows added successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - variable missing or not an array, unknown method name, or row_size differs from the existing row width.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/remove": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Remove from an array",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.remove\u0022} when the entry is removed and the array length decreases by one; later entries shift toward the head.\r\nReturns 422 ACTION_FAILED if the index is out of range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/index-of to locate the index of a value before removing, and POST /api/actions/arrays/clear to drop every entry instead.\r\n            \r\n*Example: When a chat moderator command revokes a viewer name from the squad, look up the index of the submitted name with arrays.indexOf and then remove that slot from the \u0022squad\u0022 array so the on-stream roster overlay drops the entry on the next refresh.*",
        "operationId": "Actions_ArraysRemove",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional key \u0022index\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Value removed successfully."
          },
          "422": {
            "description": "Action failed - index out of range or value not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/remove-row": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Remove a row",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.removeRow\u0022} when the row is removed; later rows shift toward the top and the row count decreases by one.\r\nReturns 422 ACTION_FAILED if the array is not two-dimensional or the index is out of range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/add-row for the inverse and POST /api/actions/arrays/clear to drop every row.\r\n            \r\n*Example: When the streamer ends a charity bracket and one supporter requests to be omitted from the recap, remove that row from the leaderboard 2D array so the on-stream table redraws without the entry.*",
        "operationId": "Actions_ArraysRemoveRow",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022index\u0022 (Integer) and \u0022num_rows\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Row removed successfully."
          },
          "422": {
            "description": "Action failed - row index out of range.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/set": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Set by index",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.set\u0022} when the slot at the requested index now holds the new value; the array length is unchanged.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the name is empty, the named variable does not exist or is not an array, the index is out of range, or the value is not coercible to the array element type (in a Vector3 array, anything but a list of at least three entries whose first three are numbers or numeric Strings giving a finite Float, or an {x, y, z} object with a number giving a finite Float for each key; a longer list reads its first three), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/get to read the prior value first and POST /api/actions/arrays/add to extend instead of overwrite.\r\n            \r\n*Example: When a viewer-submitted donation total updates the top-three Float array, set index 0 to the new leading amount so the on-stream highlight banner reflects the latest leader.*",
        "operationId": "Actions_ArraysSet",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022index\u0022 (Integer), \u0022value\u0022 (any), \u0022indexX\u0022 (Integer), and \u0022indexY\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Value set successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty, variable missing or not an array, index out of range, or value type mismatch.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/get": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Read by index",
        "description": "Returns 200 with the entry at the requested index as the response body. The value carries the array element type; a Vector3 entry is a list of three numbers [x, y, z], the form POST /api/actions/arrays/index-of takes. A Generic entry holding null (written by POST /api/actions/arrays/set or POST /api/actions/arrays/add with an omitted or null value) answers {ok: true, action: \u0022arrays.get\u0022} instead of a null body.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the name is empty, the named variable does not exist or is not an array, or the index is out of range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/size to bound the index before reading and POST /api/actions/arrays/set to write a new value.\r\n            \r\n*Example: When a chat command asks \u0022what\u0027s in slot 3 of the squad list\u0022, read the String entry at index 3 of the \u0022squad\u0022 array and forward it to the on-stream Text node for display.*",
        "operationId": "Actions_ArraysGet",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional keys \u0022index\u0022 (Integer), \u0022indexX\u0022 (Integer), and \u0022indexY\u0022 (Integer).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Value retrieved successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty, variable missing or not an array, or index out of range.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/size": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Read array length",
        "description": "Returns 200 with the bare object {sizeX, sizeY, size} where sizeX is the length along indexX, sizeY is the length along indexY, and size is sizeX multiplied by sizeY; for a 1D array sizeY is 1, so size equals the entry count. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the name is empty or the named variable does not exist or is not an array, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/resize to set a new length and POST /api/actions/arrays/clear to drop every entry.\r\n            \r\n*Example: When a viewer asks \u0022how many people are in the squad\u0022, read the length of the \u0022squad\u0022 String array and feed it into a Text node so the on-stream counter shows the current roster size.*",
        "operationId": "Actions_ArraysSize",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Size retrieved successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty, or variable missing or not an array.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/contains": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Check array membership",
        "description": "Returns 200 with a Boolean response body that is true when any entry equals the supplied value under the array element type and false otherwise.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the named array does not exist or the value is not coercible to the array element type (an array with no entries answers false without reading the value), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/index-of to also recover the position and POST /api/actions/arrays/add to extend the array when the value is missing.\r\n            \r\n*Example: When a chat command asks whether a specific viewer is already on the squad, check the \u0022squad\u0022 String array for the submitted name and route the Boolean result into a Trigger node that gates the next message.*",
        "operationId": "Actions_ArraysContains",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional key \u0022value\u0022 (any).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Membership check completed successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - array missing or value type mismatch.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/index-of": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Find index of value",
        "description": "Returns 200 with the bare object {indexX, indexY} where indexX is the row position and indexY is the position within that row of the earliest entry that equals the supplied value, or both -1 when no entry matches. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if the name is empty, the named variable does not exist or is not an array, or the value is not coercible to the array element type (in a Vector3 array, anything but a list of at least three entries whose first three are numbers or numeric Strings giving a finite Float, or an {x, y, z} object with a number giving a finite Float for each key, a longer list comparing its first three; an array with no entries answers -1 without reading the value), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/contains for a Boolean check without the position and POST /api/actions/arrays/remove to drop the entry by index.\r\n            \r\n*Example: When a viewer requests \u0022promote my name to the top of the squad\u0022, locate the index of the submitted name in the \u0022squad\u0022 String array so a follow-up set call can move it to position zero.*",
        "operationId": "Actions_ArraysIndexOf",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional key \u0022value\u0022 (any).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Index search completed successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - name empty, variable missing or not an array, or value type mismatch.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/clear": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Clear all entries",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.clear\u0022} when the array is emptied; the variable remains defined and ready for new appends.\r\nReturns 422 ACTION_FAILED if the named array does not exist, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/resize to set a specific length and POST /api/actions/arrays/remove to drop a single entry.\r\n            \r\n*Example: When the streamer ends a \u0022name the squad\u0022 segment and rolls into a new bit, clear the \u0022squad\u0022 String array so the on-stream roster overlay starts blank for the next round of chat submissions.*",
        "operationId": "Actions_ArraysClear",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array cleared successfully."
          },
          "422": {
            "description": "Action failed - array missing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/copy": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Copy an array",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.copy\u0022} when the destination holds the same length, element type, and entries as the source; the source is unchanged.\r\nReturns 422 ACTION_FAILED if the source is missing or the destination name is already in use, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/from-json to build an array from a JSON payload instead of cloning.\r\n            \r\n*Example: When the streamer snapshots the current \u0022squad\u0022 roster before opening it to a chaotic chat free-for-all, copy \u0022squad\u0022 into \u0022squad-backup\u0022 so the previous lineup can be restored later.*",
        "operationId": "Actions_ArraysCopy",
        "requestBody": {
          "description": "Required dictionary with keys \u0022name\u0022 (String, source) and \u0022target\u0022 (String, destination name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array copied successfully."
          },
          "422": {
            "description": "Action failed - source missing or target name in use.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/arrays/from-json": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Populate from JSON",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.fromJson\u0022} when the array length and entries match the parsed JSON, with each cell coerced to the array element type.\r\nReturns 422 ACTION_FAILED if the JSON is invalid, the named array does not exist, or any cell is not coercible, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/copy to clone an existing array instead and POST /api/actions/web/http-request to fetch the JSON before calling.\r\n            \r\n*Example: When the streamer pulls a fresh top-supporters list from an external donation tracker, populate the \u0022leaderboard\u0022 String array from the returned JSON payload so the on-stream overlay swaps in one step.*",
        "operationId": "Actions_ArraysFromJson",
        "requestBody": {
          "description": "Required dictionary with keys \u0022name\u0022 (String) and \u0022json\u0022 (String, a JSON array literal).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array populated successfully."
          },
          "422": {
            "description": "Action failed - JSON invalid or value coercion failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/list": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "List project assets",
        "description": "Returns 200 with the bare array of asset objects, runtime assets first and Bundled Assets after them. The response is not wrapped in an ok/action envelope.\r\nA \u0022type\u0022 outside the accepted names returns 422 ACTION_FAILED naming type and listing them, and a \u0022name\u0022 that matches nothing, or a \u0022type\u0022 no asset has, returns 200 with an empty array. Returns 400 INVALID_REQUEST if \u0022type\u0022 or \u0022name\u0022 is not a String, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/getProperty for reading individual asset fields and POST /api/actions/assets/createBundled for promoting a scene object to a Bundled Asset.\r\n            \r\n*Example: When a viewer opens a stream companion overlay, list assets filtered by type \u0022Image\u0022 so the overlay can present a thumbnail picker of available logos and stinger frames.*",
        "operationId": "Actions_AssetsList",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022type\u0022 (String, an asset type like \u0022Image\u0022 or \u0022Material\u0022 or \u0022Bundled\u0022) and \u0022name\u0022 (String, a case-insensitive substring match).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asset list returned successfully."
          },
          "400": {
            "description": "\u0022type\u0022 or \u0022name\u0022 is not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown asset type name in \u0022type\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/create": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Create a project asset",
        "description": "Returns 200 with the bare object {tag, type, name} when the asset type is recognized and creation succeeds. The response is not wrapped in an ok/action envelope.\r\nThis call takes no seed data: the new asset starts with its type\u0027s defaults.\r\nReturns 400 INVALID_REQUEST if \u0022type\u0022 or \u0022name\u0022 is missing or not a String, or \u0022folder\u0022 is not a String. Returns 422 ACTION_FAILED if \u0022type\u0022 or \u0022name\u0022 is an empty string, the asset type is unknown, or \u0022folder\u0022 is not a path under the project\u0027s on-disk asset folder (a relative folder is refused), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/addData to fill an asset after creation and POST /api/actions/assets/delete to remove one.\r\n            \r\n*Example: When a subscriber redeems a \u0022design my logo\u0022 reward, create an Image asset named after the subscriber so the next graph step can paint the redeemed artwork into it.*",
        "operationId": "Actions_AssetsCreate",
        "requestBody": {
          "description": "Required dictionary with keys \u0022type\u0022 (String, an asset type name) and \u0022name\u0022 (String, the display name). Optional key \u0022folder\u0022 (String, a folder path under the project\u0027s on-disk asset folder).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asset created successfully."
          },
          "400": {
            "description": "\u0022type\u0022 or \u0022name\u0022 missing or not a String, or \u0022folder\u0022 not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - empty type or name, unknown asset type, or folder not under the project\u0027s on-disk asset folder.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/delete": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Delete a project asset",
        "description": "Returns 200 with the bare object {tag, deleted} when a runtime asset is removed, or {name, deleted} when a Bundled Asset is removed. Bundled Assets remove the underlying .ovprefab file from disk and cannot be undone. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the asset is not found or the call targets a Bundled Asset without confirm=true, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/list to verify deletions and POST /api/actions/assets/create to recreate an asset by type.\r\n            \r\n*Example: When a mod issues a \u0022wipe the redeemed art\u0022 command after a segment ends, delete the per-viewer Image assets that built up during the segment to keep the project clean.*",
        "operationId": "Actions_AssetsDelete",
        "requestBody": {
          "description": "Required dictionary with key \u0022asset\u0022 (String, an asset tag or name). Optional key \u0022confirm\u0022 (Boolean, required when deleting a Bundled Asset).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asset deleted successfully."
          },
          "422": {
            "description": "Action failed - asset not found or unconfirmed bundled delete.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/addData": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Add data to an asset",
        "description": "Returns 200 with the bare object {tag, added} once the Image or AudioClip asset has loaded the decoded bytes, replacing its previous image or audio. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022asset\u0022 or \u0022data\u0022 is missing or is a non-null value other than a String. Returns 422 ACTION_FAILED if \u0022asset\u0022 or \u0022data\u0022 is null, the asset is not found, the asset is not an Image or AudioClip (the refusal names both), or \u0022data\u0022 is not valid base64 or decodes to no bytes, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/create to allocate an Image or AudioClip asset first and POST /api/actions/assets/list for asset tags and types.\r\n            \r\n*Example: When the streamer\u0027s overlay tool uploads a new sponsor logo, send the PNG file\u0027s bytes as a base64 string to load into the \u0022sponsor-logo\u0022 Image asset so the on-stream banner shows the new logo.*",
        "operationId": "Actions_AssetsAddData",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String) and \u0022data\u0022 (String, the file\u0027s bytes as a base64 string).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File bytes loaded into the asset."
          },
          "400": {
            "description": "\u0022asset\u0022 or \u0022data\u0022 missing, or a non-null value other than a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - asset or data null, asset not found, asset not an Image or AudioClip, or data not valid base64 or empty once decoded.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/editFilter": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Edit an asset filter",
        "description": "Returns 200 with the bare object {tag, filter, property, value} once the property on the filter at index \u0022filter\u0022 is written. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022asset\u0022, \u0022filter\u0022, \u0022property\u0022, or \u0022value\u0022 is missing, \u0022asset\u0022 or \u0022property\u0022 is not a String, or \u0022filter\u0022 is not a number. Returns 422 ACTION_FAILED if the asset is not found or the tag names an item that is not an asset, \u0022filter\u0022 is outside the asset\u0027s filter range (the refusal states the filter count), \u0022property\u0022 is not a property of that filter (the refusal lists the available names), or \u0022value\u0022 is null or cannot be converted to the property\u0027s type, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/setProperty to mutate non-filter fields and POST /api/actions/assets/addData to load underlying source data.\r\n            \r\n*Example: When a viewer redeems a \u0022make it look retro\u0022 reward, set the CountX property of the Pixelate filter on the camera feed asset so the on-stream picture turns blocky for the segment.*",
        "operationId": "Actions_AssetsEditFilter",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String, an asset tag), \u0022filter\u0022 (Integer, the zero-based filter index), \u0022property\u0022 (String, the property name on the filter), and \u0022value\u0022 (Object, the new property value).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Filter edited successfully."
          },
          "400": {
            "description": "A required key is missing or has the wrong type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - asset not found, filter index out of range, unknown filter property, or unconvertible value.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/createBundled": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Create a Bundled Asset",
        "description": "Returns 200 with the bare object {tag, name, itemCount} when the scene object is serialized to an .ovprefab on disk. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022tag\u0022 is missing or not a String, or \u0022folder\u0022 is not a String. Returns 422 ACTION_FAILED if the scene object is not found, \u0022folder\u0022 is not a path under the project\u0027s on-disk asset folder (a relative folder is refused), or the scene object is shattered, is a shattered piece, or contains either, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nShattered objects and their pieces are never saved (see POST /api/actions/scene/save), so a shattered source is refused before anything is written and no project item is added. An object becomes shattered when POST /api/actions/shatter/fragment fragments it, when POST /api/actions/shatter/demo breaks it or it breaks after POST /api/actions/shatter/activate, or when a fade from POST /api/actions/shatter/fade starts on it.\r\nSee POST /api/actions/assets/delete (with confirm=true) to remove a Bundled Asset and POST /api/actions/assets/list with type=\u0022Bundled\u0022 to enumerate them.\r\n            \r\n*Example: When a streamer designs a viewer-named character live on stream, save the finished scene object as a Bundled Asset named after the viewer so future segments can re-spawn it on demand.*",
        "operationId": "Actions_AssetsCreateBundled",
        "requestBody": {
          "description": "Required dictionary with key \u0022tag\u0022 (String, the scene object to bundle). Optional key \u0022folder\u0022 (String, a folder path under the project\u0027s on-disk asset folder).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bundled Asset created successfully."
          },
          "400": {
            "description": "\u0022tag\u0022 missing or not a String, or \u0022folder\u0022 not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - scene object not found, folder not under the project\u0027s on-disk asset folder, or scene object shattered, a shattered piece, or containing either.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/getProperty": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Read an asset property",
        "description": "Returns 200 with the bare object {tag, property, type, value} when the asset and property both resolve. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022asset\u0022 or \u0022property\u0022 is missing or is a non-null value other than a String. Returns 422 ACTION_FAILED if \u0022asset\u0022 or \u0022property\u0022 is null or an empty string, or the asset or property is not found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/setProperty for the matching write and POST /api/actions/assets/getProperties for batched reads.\r\n            \r\n*Example: When a chat command asks \u0022what color is the backdrop\u0022, read the Base Color property of the backdrop Material asset and echo the returned color through a TTS or caption node.*",
        "operationId": "Actions_AssetsGetProperty",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String, an asset tag) and \u0022property\u0022 (String, the property name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Property value returned successfully."
          },
          "400": {
            "description": "\u0022asset\u0022 or \u0022property\u0022 missing, or a non-null value other than a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - \u0022asset\u0022 or \u0022property\u0022 null or empty, or asset or property not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/setProperty": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Write an asset property",
        "description": "Returns 200 with the bare object {tag, property, type, previousValue, value} when the asset and property resolve and the value coerces. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 or \u0022property\u0022 is missing or not a String, or \u0022value\u0022 is omitted or null (a null value is refused like an omitted one before anything is forwarded, since null never clears or resets a property). Returns 422 ACTION_FAILED if \u0022object\u0022 or \u0022property\u0022 is an empty string, the asset or property is not found, or the value cannot be coerced, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/getProperty to read the value back and POST /api/actions/assets/setProperties for batched writes.\r\n            \r\n*Example: When a viewer redeems a backdrop color reward, write the chosen color to the Base Color property of the backdrop Material asset so the on-stream backdrop takes the new tint.*",
        "operationId": "Actions_AssetsSetProperty",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, an asset tag), \u0022property\u0022 (String), and \u0022value\u0022 (Object, coerced to the property type).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Property value written successfully."
          },
          "400": {
            "description": "\u0022object\u0022 or \u0022property\u0022 missing or not a String, or \u0022value\u0022 omitted or null (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - empty \u0022object\u0022 or \u0022property\u0022, asset or property not found, or value not convertible to the property type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/setProperties": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Bulk-update asset properties",
        "description": "Returns 200 with the bare object {tag, results} where each result names the property and carries either its type, previous value, and new value, or an error reason for that property. The response is not wrapped in an ok/action envelope.\r\nBest-effort semantics apply: an entry without a name, naming no property of the asset, or carrying a value that cannot be coerced gets an error in its result without stopping the remaining writes, and the asset is saved once after the batch.\r\nEvery entry needs a value: an entry that omits value or sets it to null refuses the whole request before any property is written, and the error names that entry as properties[i]; a null value is refused like an omitted one, since null never clears or resets a property.\r\nReturns 400 INVALID_REQUEST if \u0022asset\u0022 or \u0022properties\u0022 is missing or not a String, or an entry of the \u0022properties\u0022 array has no value. Returns 422 ACTION_FAILED if \u0022asset\u0022 is an empty string, the asset is not found, or \u0022properties\u0022 is not a JSON array of {name, value} objects or is empty, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/setProperty for single-property writes and POST /api/actions/assets/getProperties for batched reads.\r\n            \r\n*Example: When a subscriber redeems a \u0022remix my scene\u0022 reward, write the Base Color and Smoothness properties on the stage floor Material asset in one call, then check each result\u0027s error field, since a property that fails does not undo the ones that landed.*",
        "operationId": "Actions_AssetsSetProperties",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String) and \u0022properties\u0022 (String, a JSON-encoded array of {name, value} pairs).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Properties written; each result carries the new value or its own error."
          },
          "400": {
            "description": "\u0022asset\u0022 or \u0022properties\u0022 missing or not a String, or an entry without a value (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - empty \u0022asset\u0022, asset not found, or \u0022properties\u0022 not a non-empty JSON array of {name, value} objects.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/getShader": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Read material shader",
        "description": "Returns 200 with the bare object {asset, shader, assetType} naming the active shader and the host asset kind. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the asset has no material slot, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/changeShader for the matching write and POST /api/actions/assets/listShaders to enumerate the valid shader names.\r\n            \r\n*Example: When a stream companion overlay opens a material picker for a viewer, read the current shader on the highlighted asset so the picker can preselect the right option.*",
        "operationId": "Actions_AssetsGetShader",
        "requestBody": {
          "description": "Required dictionary with key \u0022asset\u0022 (String, an asset tag pointing to a Material or a material-bearing asset).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shader name returned successfully."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/changeShader": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Change material shader",
        "description": "Returns 200 with the bare object {asset, previousShader, shader} when the shader name resolves and is applied. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the asset has no material slot or the shader name is unknown, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/listShaders for the valid shader names and POST /api/actions/assets/getShader to verify the change.\r\n            \r\n*Example: When a viewer hits a \u0022go shiny\u0022 channel-point reward, change the shader on the streamer-avatar material to a metallic variant so the on-stream model takes on a glossy finish for the segment.*",
        "operationId": "Actions_AssetsChangeShader",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String, an asset tag) and \u0022shader\u0022 (String, the full case-sensitive shader name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shader changed successfully."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/listShaders": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "List available shaders",
        "description": "Returns 200 with the bare object {shaders, count} naming every shader compatible with the optional type filter. The response is not wrapped in an ok/action envelope.\r\nA \u0022type\u0022 that is not an asset type name returns 422 ACTION_FAILED naming type and listing the accepted names. Returns 400 INVALID_REQUEST if \u0022type\u0022 is not a String, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/changeShader to apply one of the listed names and POST /api/actions/assets/getShader to read the active shader before swapping.\r\n            \r\n*Example: When a stream companion overlay opens a \u0022pick a vibe\u0022 shader picker for a viewer command, list the shaders filtered by the asset type so the picker only shows valid choices.*",
        "operationId": "Actions_AssetsListShaders",
        "requestBody": {
          "description": "Optional dictionary with key \u0022type\u0022 (String, an asset type filter that scopes the returned shaders).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shader list returned successfully."
          },
          "400": {
            "description": "\u0022type\u0022 is not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown asset type name in \u0022type\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/assets/getProperties": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Bulk-read asset properties",
        "description": "Returns 200 with the bare object {asset, results, errors} so callers can render mixed success and failure outcomes in one pass, where each results entry carries {name, type, value} and each errors entry carries {name, error}. The response is not wrapped in an ok/action envelope.\r\nA name the asset does not have is listed in errors and does not fail the request.\r\nReturns 400 INVALID_REQUEST if \u0022asset\u0022 or \u0022properties\u0022 is missing, or \u0022asset\u0022 is a non-null value other than a String. Returns 422 ACTION_FAILED if \u0022asset\u0022 is null or an empty string, the asset is not found, or \u0022properties\u0022 is null, a String, a number, or a Boolean, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/getProperty for single reads and POST /api/actions/assets/setProperties for the matching batched write.\r\n            \r\n*Example: When a stream companion overlay opens a detail panel for a redeemed backdrop, read the Base Color, Smoothness, and Global Texture Scale properties of its Material asset in one call so the panel populates in a single round-trip instead of three.*",
        "operationId": "Actions_AssetsGetProperties",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String) and \u0022properties\u0022 (array of property name strings).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Properties returned; each name appears in results or in errors."
          },
          "400": {
            "description": "\u0022asset\u0022 or \u0022properties\u0022 missing, or \u0022asset\u0022 a non-null value other than a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - \u0022asset\u0022 null or empty, asset not found, or \u0022properties\u0022 null, a String, a number, or a Boolean.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/audio/play": {
      "post": {
        "tags": [
          "L2 Audio: Actions"
        ],
        "summary": "Play an audio clip",
        "description": "Returns 200 with {ok: true, action: \u0022audio.play\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no AudioPlayer3D, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe clip, volume, pitch, and loop settings are configured on the AudioPlayer3D in the scene; this endpoint only triggers playback. See POST /api/actions/audio/stop to halt playback started here.\r\n            \r\n*Example: When a viewer redeems a \u0022play my hype song\u0022 channel point reward, trigger this on the AudioPlayer3D attached to a scene speaker so the assigned track starts playing.*",
        "operationId": "Actions_AudioPlay",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audio playback started."
          },
          "422": {
            "description": "Action failed - e.g., no AudioPlayer3D on the named object.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/audio/stop": {
      "post": {
        "tags": [
          "L2 Audio: Actions"
        ],
        "summary": "Stop audio playback",
        "description": "Returns 200 with {ok: true, action: \u0022audio.stop\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no AudioSource, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/audio/play for the corresponding start operation.\r\n            \r\n*Example: When a TTS donation alert begins, stop the background music AudioSource so the spoken message lands clearly, then restart it from the alert\u0027s completion handler.*",
        "operationId": "Actions_AudioStop",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audio stopped."
          },
          "422": {
            "description": "Action failed - e.g., no AudioSource on the named object.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/audio/decibel": {
      "post": {
        "tags": [
          "L2 Audio: Actions"
        ],
        "summary": "Read decibel level",
        "description": "Returns 200 with the bare object {decibel} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022object\u0022 is empty, no scene object matches it, or the scene object has no AudioVisualizer component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. The scene object is the only input; there is no channel selector.\r\nThe reading reflects the current frame; poll this endpoint to drive a continuous overlay, or read the level inside a graph with the Decibel Level node.\r\n            \r\n*Example: When the streamer\u0027s microphone level crosses a loudness threshold, sample this each tick and feed the value into an Interpolate node that brightens a scene light so the on-stream visuals react to shouting.*",
        "operationId": "Actions_AudioDecibel",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decibel level retrieved."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., no scene object matches \u0022object\u0022 or it has no AudioVisualizer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/camera/switch": {
      "post": {
        "tags": [
          "L2 Camera: Actions"
        ],
        "summary": "Switch active camera",
        "description": "Returns 200 with {ok: true, action: \u0022camera.switch\u0022} on success.\r\nReturns 400 INVALID_REQUEST if \u0022camera\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022transition\u0022 is not None or one of the transition names, or if the named Camera scene object does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe previously active Camera is deactivated in the same step. See POST /api/actions/scene/switch for switching the entire scene rather than just the active Camera.\r\n            \r\n*Example: When a chat command says \u0022go to the front cam\u0022, switch the active Camera to FrontStageCam so the on-stream view cuts to the new angle without reloading the scene or respawning props.*",
        "operationId": "Actions_CameraSwitch",
        "requestBody": {
          "description": "Required dictionary with key \u0022camera\u0022 (String, the target Camera scene-object name or identifier); optional key \u0022transition\u0022 (String, transition to play during the switch, or None for an immediate cut).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Camera switched successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022camera\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed because \u0022transition\u0022 is not one of its names or the named Camera did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/components/add": {
      "post": {
        "tags": [
          "L2 Components: Actions"
        ],
        "summary": "Add a component",
        "description": "Returns 200 with {ok: true, action: \u0022components.add\u0022} on success.\r\nReturns 422 ACTION_FAILED if the type name does not resolve to a supported component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe component is added in its default state; follow with POST /api/actions/components/reset to restore defaults or with property writes to configure it. See POST /api/actions/components/remove for the inverse operation.\r\n            \r\n*Example: When a viewer redeems a \u0022make the streamer fall\u0022 channel point reward, add a Rigidbody to the streamer scene object so gravity takes over for the next bit.*",
        "operationId": "Actions_ComponentsAdd",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022component\u0022 (String, the component type name to add).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component added successfully."
          },
          "422": {
            "description": "Action failed - e.g., invalid component type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/components/remove": {
      "post": {
        "tags": [
          "L2 Components: Actions"
        ],
        "summary": "Remove a component",
        "description": "Returns 200 with {ok: true, action: \u0022components.remove\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named component is not present on the target, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nOnly the first matching component on the scene object is removed; references held elsewhere become invalid. See POST /api/actions/components/add for the inverse operation.\r\n            \r\n*Example: When a chat moderator types !nophysics to calm a chaotic on-stream prop, remove the Rigidbody from the prop scene object so it stops responding to forces.*",
        "operationId": "Actions_ComponentsRemove",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022component\u0022 (String, the component type name to remove).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component removed successfully."
          },
          "422": {
            "description": "Action failed - e.g., component not found on object.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/components/reset": {
      "post": {
        "tags": [
          "L2 Components: Actions"
        ],
        "summary": "Reset a component",
        "description": "Returns 200 with the bare object {tag, component, reset} on success, where tag echoes the requested object identifier, component echoes the requested type name, and reset is always true. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the named component is not present on the target, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nAll editable fields revert to the component\u0027s reset defaults; serialized references on the component are also cleared. See POST /api/actions/components/enable to flip the enabled flag without otherwise mutating state.\r\n            \r\n*Example: When a donation closes a \u0022remix my lighting\u0022 segment, reset the scene Light component back to its default color and intensity so the next segment starts from a known baseline.*",
        "operationId": "Actions_ComponentsReset",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022component\u0022 (String, the component type name to reset).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component reset successfully."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/components/enable": {
      "post": {
        "tags": [
          "L2 Components: Actions"
        ],
        "summary": "Set component enabled state",
        "description": "Returns 200 with {ok: true, action: \u0022components.enable\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named component is not present or does not expose an enabled flag, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nToggling the flag pauses or resumes the component\u0027s update cycle without removing it; references to the component remain valid across the change. See POST /api/actions/components/reset for a deeper revert.\r\n            \r\n*Example: When a channel point reward turns on a hidden-from-stream \u0022secret camera\u0022 Collider, set enabled=true on the Collider so it intercepts raycasts for the duration of the reward.*",
        "operationId": "Actions_ComponentsEnable",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022component\u0022 (String, the component type name). Optional key \u0022enabled\u0022 (Boolean, the new state; defaults to toggling the current state when omitted).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component enable state changed."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/components/refresh": {
      "post": {
        "tags": [
          "L2 Components: Actions"
        ],
        "summary": "Refresh visual components",
        "description": "Returns 200 with the bare object {object, refreshed} on success, where refreshed lists the component type names. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the target scene object cannot be resolved, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nCall after batching property writes on AudioVisualizer, DynamicShadowBoxModeler, VectorShape, or TextElement3d to apply the staged updates as a single visual rebuild. Components that do not require an explicit rebuild are skipped silently.\r\n            \r\n*Example: When a subscriber milestone triggers a multi-field swap on the on-stream VectorShape banner, refresh the target object after the property writes so the banner repaints in one frame rather than mid-animation.*",
        "operationId": "Actions_ComponentsRefresh",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the target scene object identifier).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns list of refreshed component types."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/convert/type": {
      "post": {
        "tags": [
          "L2 Convert: Actions"
        ],
        "summary": "Convert a value",
        "description": "Returns 200 with the bare converted value as the response body. The response carries the {ok, action} envelope only when the result is null, which happens when \u0022to\u0022 is String and \u0022value\u0022 is omitted or null. A Vector3 result is a bare [x, y, z] array, and a single number converted to Vector3 fills all three components.\r\nReturns 400 INVALID_REQUEST if \u0022to\u0022 is missing or not a String, 422 ACTION_FAILED if the value cannot be converted to the target type or \u0022to\u0022 names an unsupported type, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nColor is not a target of this endpoint: to turn a hex string such as \u0022#FF0000\u0022 into a Color, call POST /api/actions/data/primitive-value with type \u0022Color\u0022.\r\n            \r\n*Example: When a viewer types a spin speed such as \u00222.5\u0022 in chat, convert the String into a Float and feed it to a rotation node that spins an on-stream scene element.*",
        "operationId": "Actions_ConvertType",
        "requestBody": {
          "description": "Required dictionary with key \u0022to\u0022 (String, the target canonical type name). Optional key \u0022value\u0022 (any, the value to convert).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion successful."
          },
          "400": {
            "description": "Missing or non-String \u0022to\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed, e.g., a value that cannot convert or an unsupported target type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/data/primitive-value": {
      "post": {
        "tags": [
          "L2 Data: Actions"
        ],
        "summary": "Create a primitive value",
        "description": "Returns 200 with the bare typed value as the response body. The response carries the {ok, action} envelope only when the result is null: an omitted or null \u0022value\u0022 with type String (the default). Vector3 is a bare [x, y, z] array and Color a bare [r, g, b, a] array; a hex string or color name yields channels from 0 to 1 (\u0022red\u0022 gives [1, 0, 0, 1]), array and object channels pass through as sent, a missing r, g, or b channel becomes 0, and a missing alpha becomes 1.\r\nReturns 422 ACTION_FAILED when \u0022type\u0022 is not one of the accepted names, when an Integer, Float, or Boolean value is a String that does not parse, an array, or an object, when an Integer falls outside the 32-bit range, when a Vector3 value is neither an [x, y, z] array whose every component is a number or numeric String giving a finite Float nor an {x, y, z} object whose every component is a number giving a finite Float, or when a Color entry does not convert to a Float (null, a nested array or object, or a String that does not parse), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. Missing or null Vector3 input returns [0, 0, 0]; missing Color input, a string that is neither a hex color (#RGB, #RRGGBB, #RGBA, #RRGGBBAA) nor a color name the parser accepts (such as \u0022red\u0022), or a value that is neither a string, an array, nor an object returns opaque black [0, 0, 0, 1]; both answer 200.\r\nCanonical type names match GET /api/types; see POST /api/actions/convert/type for converting an existing value between types.\r\n            \r\n*Example: When a chat command such as \u0022!goal 500\u0022 sets a target sub goal, wrap the \u0022500\u0022 text as an Integer with this endpoint and write the result to a \u0022sub_goal\u0022 Integer variable through POST /api/actions/variables/set so it drives an on-stream progress bar.*",
        "operationId": "Actions_DataPrimitiveValue",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022type\u0022 (String, case-insensitive, one of Boolean, Float, Integer, String, Vector3, Color, or the short forms bool, int, vector; defaults to String) and \u0022value\u0022 (any, the literal payload to wrap).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Primitive value created."
          },
          "422": {
            "description": "Action failed, e.g., an unknown type name, an Integer, Float, or Boolean String that does not parse, an array or object for one of those types, an Integer outside the 32-bit range, a Vector3 value that is neither an [x, y, z] array of finite numbers or numeric Strings nor an {x, y, z} object of finite numbers, or a Color entry that does not convert to a Float.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/datetime/get": {
      "post": {
        "tags": [
          "L2 Datetime: Actions"
        ],
        "summary": "Get current date and time",
        "description": "Returns 200 with the bare object {time, month, day, year, formatted} as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the format pattern is invalid, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nWith no format (or an empty one), formatted is the .NET round-trip ISO 8601 form \u0022o\u0022 whatever the machine\u0027s culture: for example \u00222026-09-27T15:04:05.1234567Z\u0022 when utc is true, and the local time with its offset, such as \u00222026-09-27T11:04:05.1234567-04:00\u0022, when utc is false or omitted.\r\n            \r\n*Example: When stream starts, read the current UTC timestamp and feed the formatted value into a String variable that drives an on-stream \u0022we are live since\u0022 overlay.*",
        "operationId": "Actions_DatetimeGet",
        "requestBody": {
          "description": "Optional dictionary with key \u0022format\u0022 (String, a .NET date/time format pattern) and key \u0022utc\u0022 (Boolean, true for UTC, false for local time).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Date/time retrieved."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/files/read": {
      "post": {
        "tags": [
          "L2 Files: Actions"
        ],
        "summary": "Read a file",
        "description": "Returns 200 with the bare string contents as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022path\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022path\u0022 is an empty String, \u0022mode\u0022 is not one of its names, the path does not exist, or access is denied, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/files/write for the corresponding write operation.\r\n            \r\n*Example: When a viewer redeems a \u0022name on screen\u0022 channel point reward, read a curated greeting from a local file and feed it into a String variable that drives an on-stream caption.*",
        "operationId": "Actions_FilesRead",
        "requestBody": {
          "description": "Required dictionary with key \u0022path\u0022 (String). Optional key \u0022mode\u0022 (String, the read mode such as \u0022text\u0022 or \u0022bytes\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File read successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022path\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown mode name, file not found, or access denied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/files/write": {
      "post": {
        "tags": [
          "L2 Files: Actions"
        ],
        "summary": "Write a file",
        "description": "Returns 200 with {ok: true, action: \u0022files.write\u0022} on success.\r\nReturns 422 ACTION_FAILED if \u0022mode\u0022 is not one of its names, neither \u0022path\u0022 nor both \u0022folder\u0022 and \u0022file_name\u0022 are supplied, the path is invalid, or access is denied, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nUse \u0022mode\u0022: \u0022binary\u0022 with a base64-encoded \u0022data\u0022 payload to write raw binary files. See POST /api/actions/files/read for the corresponding read operation.\r\n            \r\n*Example: When a donation event lands, write the donor name and amount to a CSV that an on-stream \u0022recent supporters\u0022 overlay polls for updates.*",
        "operationId": "Actions_FilesWrite",
        "requestBody": {
          "description": "Optional keys \u0022path\u0022 (String, full file path), \u0022folder\u0022 (String, folder of the destination file), \u0022file_name\u0022 (String, file name within folder), \u0022content\u0022 (String), \u0022mode\u0022 (String, \u0022text\u0022, \u0022bytes\u0022, or \u0022binary\u0022), and \u0022data\u0022 (String, base64-encoded binary alias for content). Supply \u0022path\u0022, or \u0022folder\u0022 with \u0022file_name\u0022.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File written successfully."
          },
          "422": {
            "description": "Action failed - e.g., unknown mode name, access denied, or invalid path.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/generators/random": {
      "post": {
        "tags": [
          "L2 Generators: Actions"
        ],
        "summary": "Generate a random value",
        "description": "Returns 200 with the bare generated value as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if \u0022type\u0022 is not one of float, int, integer, or vector3 (the message names the accepted values), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/generators/wave for periodic signal generation and GET /api/types for the canonical type names.\r\n            \r\n*Example: When a viewer cheers, generate a random Vector3 within scene bounds and feed it to a \u0022spawn position\u0022 port to drop a celebratory prop at an unpredictable spot on stream.*",
        "operationId": "Actions_GeneratorsRandom",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022min\u0022 (Float), \u0022max\u0022 (Float), and \u0022type\u0022 (String, one of \u0022float\u0022, \u0022int\u0022, \u0022integer\u0022, \u0022vector3\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Random value generated."
          },
          "422": {
            "description": "Action failed - e.g., an unrecognized type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/generators/wave": {
      "post": {
        "tags": [
          "L2 Generators: Actions"
        ],
        "summary": "Generate a wave signal",
        "description": "Returns 200 with the bare sampled Float value as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the requested waveform is unsupported, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nWith neither \u0022min\u0022 nor \u0022max\u0022 sent, \u0022amplitude\u0022 and \u0022offset\u0022 set the output range, which defaults to -1.0 to 1.0. Sine, square, and triangle waves reach both ends of the range; a sawtooth rises from the low end and wraps back before reaching the high end.\r\nSee POST /api/actions/generators/random for one-shot randomization and POST /api/actions/transform/set for applying a sampled value to a transform.\r\n            \r\n*Example: When a chat command starts a \u0022pulse\u0022 effect, sample a sine wave at 2 Hz and feed the value into a light intensity port to pulse the on-stream overlay in time with the action.*",
        "operationId": "Actions_GeneratorsWave",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022type\u0022 (String, one of \u0022sine\u0022, \u0022square\u0022, \u0022triangle\u0022, \u0022sawtooth\u0022), \u0022frequency\u0022 (Float, Hz), \u0022amplitude\u0022 (Float), \u0022offset\u0022 (Float), \u0022time\u0022 (Float), \u0022min\u0022 (Float), and \u0022max\u0022 (Float).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Wave value generated."
          },
          "422": {
            "description": "Action failed - e.g., an unsupported waveform type.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/log/console": {
      "post": {
        "tags": [
          "L2 Log: Actions"
        ],
        "summary": "Write to in-app console",
        "description": "Returns 200 with {ok: true, action: \u0022log.console\u0022} on success.\r\nReturns 400 INVALID_REQUEST if \u0022text\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022text\u0022 is an empty String, \u0022level\u0022 is not one of its names (nothing is written), or the message cannot be written, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/log/log for the lower-level debug log stream.\r\n            \r\n*Example: When a chat command fires a graph that performs a multi-step scene change, post a \u0022starting effect\u0022 line to the in-app console so the streamer sees the trigger landed.*",
        "operationId": "Actions_LogConsole",
        "requestBody": {
          "description": "Required dictionary with key \u0022text\u0022 (String). Optional key \u0022level\u0022 (String, one of \u0022info\u0022, \u0022warning\u0022, \u0022error\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message written to console."
          },
          "400": {
            "description": "Missing or non-String \u0022text\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown level name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/log/log": {
      "post": {
        "tags": [
          "L2 Log: Actions"
        ],
        "summary": "Write to debug console",
        "description": "Returns 200 with {ok: true, action: \u0022log.log\u0022} on success.\r\nReturns 422 ACTION_FAILED if the message cannot be written, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/log/console for the streamer-visible in-app console.\r\n            \r\n*Example: When debugging a viewer-triggered graph, log each branch decision to the debug console so the trace appears in the developer pane without cluttering the on-stream feedback.*",
        "operationId": "Actions_LogLog",
        "requestBody": {
          "description": "Required dictionary with key \u0022text\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message logged."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/midi/send": {
      "post": {
        "tags": [
          "L2 Midi: Actions"
        ],
        "summary": "Send a MIDI message",
        "description": "Returns 200 with {ok: true, action: \u0022midi.send\u0022} on success.\r\nReturns 422 ACTION_FAILED when event_type is missing or unsupported, ProgramChange has no program, a value is outside its range (the error names the key and the range), or no MIDI output device is available, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nA refused call sends nothing. See POST /api/actions/midi/devices to enumerate available outputs and POST /api/actions/midi/subscribe for inbound MIDI events.\r\n            \r\n*Example: When a donation amount crosses a threshold, send a MIDI note-on at a velocity scaled to the amount so an external lighting console fires a celebration cue keyed to the size of the tip.*",
        "operationId": "Actions_MidiSend",
        "requestBody": {
          "description": "Optional keys \u0022event_type\u0022 (String), \u0022channel\u0022 (Integer, MIDI channel 1 to 16), \u0022note\u0022 (Integer), \u0022velocity\u0022 (Integer), \u0022number\u0022 (Integer), \u0022value\u0022 (Integer), \u0022program\u0022 (Integer), \u0022bend\u0022 (Integer), and \u0022device\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MIDI message sent."
          },
          "422": {
            "description": "Action failed, for example a value out of range or no MIDI device connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/midi/subscribe": {
      "post": {
        "tags": [
          "L2 Midi: Actions"
        ],
        "summary": "Subscribe to MIDI input",
        "description": "Returns 200 with the bare object {subscriptionId, device, status} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the device is not found or a subscription for the same device already exists, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nInbound messages are broadcast as \u0022midi.input\u0022 L3 events over the WebSocket hub, rate-limited to 100 events per second with duplicate suppression. Use the returned subscriptionId with POST /api/actions/midi/unsubscribe to stop delivery, and POST /api/actions/midi/devices to discover input device names.\r\n            \r\n*Example: When the streamer plugs in an Akai Launchpad to drive scene cues, subscribe to its input so each pad press surfaces as a midi.input event that a graph maps to OBS scene swaps.*",
        "operationId": "Actions_MidiSubscribe",
        "requestBody": {
          "description": "Required dictionary with key \u0022device\u0022 (String) naming the MIDI input device to attach to.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription created. Returns subscriptionId."
          },
          "422": {
            "description": "Action failed - e.g., device not found or already subscribed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/midi/unsubscribe": {
      "post": {
        "tags": [
          "L2 Midi: Actions"
        ],
        "summary": "Unsubscribe from MIDI input",
        "description": "Returns 200 with the bare object {subscriptionId, status} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the subscriptionId is unknown or already released, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nAfter this call no further midi.input events fire for that subscription. See POST /api/actions/midi/subscribe to start a new listener and POST /api/actions/midi/devices to enumerate available inputs.\r\n            \r\n*Example: When a chat moderator types !nopad to disable the on-stream MIDI control surface during a guest segment, unsubscribe the prior Launchpad subscription so the controller stops triggering scene swaps.*",
        "operationId": "Actions_MidiUnsubscribe",
        "requestBody": {
          "description": "Required dictionary with key \u0022subscriptionId\u0022 (String) returned by an earlier midi.subscribe call.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription removed."
          },
          "422": {
            "description": "Action failed - e.g., invalid subscription ID.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/midi/devices": {
      "post": {
        "tags": [
          "L2 Midi: Actions"
        ],
        "summary": "List MIDI devices",
        "description": "Returns 200 with the bare object {inputDevices, outputDevices} on success, where each entry carries a name field. The response is not wrapped in an ok/action envelope.\r\nAn enumeration failure is not refused: if the host MIDI subsystem cannot enumerate inputs or outputs, the failure is logged and the call still returns 200, with that list holding only the devices read before the failure (usually none). Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nDevice names returned here are the literals accepted by the \u0022device\u0022 parameter of POST /api/actions/midi/send and POST /api/actions/midi/subscribe.\r\n            \r\n*Example: When the streamer opens a pre-show diagnostics overlay, list MIDI devices and bind each entry to a button so missing or renamed hardware is caught before going live.*",
        "operationId": "Actions_MidiDevices",
        "requestBody": {
          "description": "Empty or unused dictionary; this endpoint takes no parameters.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device lists returned."
          },
          "422": {
            "description": "Action failed on the OverMox side; a device enumeration failure returns 200 instead, as described in the remarks.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/output/get": {
      "post": {
        "tags": [
          "L2 Output: Actions"
        ],
        "summary": "Read output settings",
        "description": "Returns 200 with the bare settings object on success: resolutionX and resolutionY are the output size in pixels, frameRate is the target frame rate (-1 when unrestricted), and the five Booleans are the vSync, Spout, NDI, markup overlay, and transparent background states. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the read cannot complete, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/output/set to change several fields in one call and POST /api/actions/output/toggle to set or flip one Boolean field.\r\n            \r\n*Example: When the streamer\u0027s OBS control panel opens a \u0022live output\u0022 dashboard, call this to populate the current resolution, frame rate, and Spout/NDI indicators in one round trip.*",
        "operationId": "Actions_OutputGet",
        "requestBody": {
          "description": "Empty dictionary; no parameters required.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current output settings returned."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/output/set": {
      "post": {
        "tags": [
          "L2 Output: Actions"
        ],
        "summary": "Update output settings",
        "description": "Returns 200 with the bare settings object on success, carrying the full output settings record after the change. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST before dispatch when a provided value has the wrong JSON type (for example a string frameRate), 422 ACTION_FAILED if any provided value is out of range (for example frameRate \u003C 15 and not -1, or a non-positive resolution), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/output/get for the current values and POST /api/actions/output/toggle for boolean fields.\r\n            \r\n*Example: When a viewer redeems a \u0022cinematic mode\u0022 channel point reward, set resolutionX and resolutionY to a wider aspect ratio so the on-stream framing shifts for the duration of the bit.*",
        "operationId": "Actions_OutputSet",
        "requestBody": {
          "description": "Optional keys \u0022resolutionX\u0022 (Integer, \u003E 0), \u0022resolutionY\u0022 (Integer, \u003E 0), \u0022frameRate\u0022 (Integer, \u003E= 15 or -1 for unrestricted), \u0022vSyncEnabled\u0022 (Boolean), \u0022spoutEnabled\u0022 (Boolean), \u0022ndiEnabled\u0022 (Boolean), \u0022showMarkup\u0022 (Boolean), and \u0022transparentBGEnabled\u0022 (Boolean). Omitted fields remain unchanged.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated output settings returned."
          },
          "422": {
            "description": "Action failed - e.g., invalid frame rate or resolution.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "A provided value has the wrong JSON type (INVALID_REQUEST)."
          }
        }
      }
    },
    "/api/actions/output/toggle": {
      "post": {
        "tags": [
          "L2 Output: Actions"
        ],
        "summary": "Toggle a boolean output setting",
        "description": "Returns 200 with the bare settings object on success, carrying the full output settings record after the change. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST before dispatch when \u0022setting\u0022 is missing or not a string, or \u0022enabled\u0022 is neither a Boolean nor null, 422 ACTION_FAILED if the setting name is not recognized, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/output/set for numeric fields and POST /api/actions/output/get for the current state.\r\n            \r\n*Example: When a viewer subscribes, set spout with enabled true to push a celebratory feed into a downstream compositor, then thirty seconds later send spout with enabled omitted to flip it back off.*",
        "operationId": "Actions_OutputToggle",
        "requestBody": {
          "description": "Required dictionary with key \u0022setting\u0022 (String, one of \u0022vsync\u0022, \u0022spout\u0022, \u0022ndi\u0022, \u0022showMarkup\u0022, \u0022transparentBG\u0022). Optional key \u0022enabled\u0022 (Boolean); when omitted or null the runtime flips the current state.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated output settings returned."
          },
          "422": {
            "description": "Action failed - e.g., unknown setting name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "\u0022setting\u0022 is missing or not a string, or \u0022enabled\u0022 is neither a Boolean nor null (INVALID_REQUEST)."
          }
        }
      }
    },
    "/api/actions/particles/play": {
      "post": {
        "tags": [
          "L2 Particles: Actions"
        ],
        "summary": "Play a particle system",
        "description": "Returns 200 with {ok: true, action: \u0022particles.play\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no ParticleSystem component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/particles/stop to halt emission and POST /api/actions/particles/clear to drop existing particles.\r\n            \r\n*Example: When a viewer subscribes, play a confetti ParticleSystem attached to a scene celebration prop so the on-stream visuals burst at the moment of the alert.*",
        "operationId": "Actions_ParticlesPlay",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Particle system started."
          },
          "422": {
            "description": "Action failed - e.g., no ParticleSystem component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/particles/pause": {
      "post": {
        "tags": [
          "L2 Particles: Actions"
        ],
        "summary": "Pause a particle system",
        "description": "Returns 200 with {ok: true, action: \u0022particles.pause\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no ParticleSystem component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nExisting particles remain visible and frozen in place; resume by calling POST /api/actions/particles/play again.\r\n            \r\n*Example: When a chat moderator types !freeze during a hype moment, pause the active fireworks ParticleSystem so the on-stream burst hangs in mid-air until the moderator lifts the pause.*",
        "operationId": "Actions_ParticlesPause",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Particle system paused."
          },
          "422": {
            "description": "Action failed - e.g., no ParticleSystem component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/particles/stop": {
      "post": {
        "tags": [
          "L2 Particles: Actions"
        ],
        "summary": "Stop a particle system",
        "description": "Returns 200 with {ok: true, action: \u0022particles.stop\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no ParticleSystem component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nStops new emission while leaving in-flight particles to live out their lifetime. See POST /api/actions/particles/clear for a dedicated clear without changing emission state.\r\n            \r\n*Example: When a raid stinger finishes its scripted window, stop the incoming-raid ParticleSystem with clear=true so the on-stream burst disappears cleanly before the next scene loads.*",
        "operationId": "Actions_ParticlesStop",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Particle system stopped."
          },
          "422": {
            "description": "Action failed - e.g., no ParticleSystem component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/particles/clear": {
      "post": {
        "tags": [
          "L2 Particles: Actions"
        ],
        "summary": "Clear particle system",
        "description": "Returns 200 with {ok: true, action: \u0022particles.clear\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no ParticleSystem component, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nRemoves currently visible particles immediately without changing the emission state; a still-playing system will keep spawning new particles after the clear. See POST /api/actions/particles/stop to halt emission and clear in one call.\r\n            \r\n*Example: When a channel point reward swaps the on-stream theme mid-scene, clear the leftover sparkle ParticleSystem from the prior theme so the new visuals start on a clean frame.*",
        "operationId": "Actions_ParticlesClear",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Particles cleared."
          },
          "422": {
            "description": "Action failed - e.g., no ParticleSystem component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/addForce": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Apply a Rigidbody force",
        "description": "Returns 200 with {ok: true, action: \u0022physics.addForce\u0022} on success.\r\nA scene object without a Rigidbody gets one before the force is applied, and a kinematic Rigidbody is made non-kinematic. Returns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found, \u0022force_type\u0022 or \u0022force_mode\u0022 is not one of its names, or \u0022force\u0022 or \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe force vector is interpreted in world space, or in the Rigidbody\u0027s local space when force_type is \u0022RelativeForce\u0022; force_mode defaults to \u0022Force\u0022 (continuous over a frame) and maps to ForceMode values. See POST /api/actions/physics/addExplosion for radial impulses and POST /api/actions/physics/setVelocity for direct velocity overrides.\r\n            \r\n*Example: When a viewer redeems a \u0022shove the streamer\u0022 channel point reward, apply an Impulse force along the camera-forward axis to the streamer avatar Rigidbody so the on-stream character lurches in the requested direction.*",
        "operationId": "Actions_PhysicsAddForce",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022force\u0022 (Vector3, defaults to Vector3.zero when omitted), \u0022force_type\u0022 (String, one of \u0022Force\u0022, \u0022RelativeForce\u0022, \u0022Torque\u0022, \u0022ForceAtPosition\u0022 in any letter case, defaults to \u0022Force\u0022), \u0022force_mode\u0022 (String, one of \u0022Force\u0022, \u0022Impulse\u0022, \u0022VelocityChange\u0022, \u0022Acceleration\u0022 in any letter case, defaults to \u0022Force\u0022), and \u0022position\u0022 (Vector3, used when force_type is \u0022ForceAtPosition\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Force applied successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., scene object not found, unknown force_type or force_mode name, or malformed Vector3 parameter.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/addExplosion": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Apply a radial explosion",
        "description": "Returns 200 with {ok: true, action: \u0022physics.addExplosion\u0022} on success.\r\nA scene object without a Rigidbody gets one before the explosion is applied, and a kinematic Rigidbody is made non-kinematic. Returns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found, \u0022force_mode\u0022 is not one of its names, \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or the explosion cannot be applied for any other reason, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe force falls off with distance from the origin inside the radius, and a radius of 0 applies the full force at any distance; upwards_mod lowers the apparent origin to bias trajectories upward and defaults to zero when omitted. Only the Rigidbody on the named scene object is pushed, so scattering several scene objects takes one call per object. See POST /api/actions/physics/addForce for a push along a single direction.\r\n            \r\n*Example: When a donation crosses a \u0022blow up the set\u0022 tier, apply an explosion force at the prop pile origin with a large radius to each loose prop in turn so every prop on stage scatters away from the blast point.*",
        "operationId": "Actions_PhysicsAddExplosion",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3, defaults to [0, 0, 0], the world origin), \u0022force\u0022 (Float, defaults to 0), \u0022radius\u0022 (Float, defaults to 0), \u0022upwards_mod\u0022 (Float, defaults to 0), and \u0022force_mode\u0022 (String, one of \u0022Force\u0022, \u0022Impulse\u0022, \u0022VelocityChange\u0022, \u0022Acceleration\u0022 in any letter case, defaults to \u0022Force\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Explosion force applied."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/setVelocity": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Set Rigidbody velocity",
        "description": "Returns 200 with {ok: true, action: \u0022physics.setVelocity\u0022} on success.\r\nA scene object without a Rigidbody gets one before the velocity is set, and a kinematic Rigidbody is made non-kinematic. Returns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022velocity\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nOverwrites the existing velocity vector rather than adding to it; pass a zero Vector3 to bring the Rigidbody to a stop without disabling physics. See POST /api/actions/physics/addForce for accumulating impulses and POST /api/actions/physics/getVelocity to read the current value.\r\n            \r\n*Example: When a chat command names a target speed during a \u0022drive the cart\u0022 segment, set the cart Rigidbody velocity to a forward Vector3 of the requested magnitude so the on-stream prop matches the viewer\u0027s call.*",
        "operationId": "Actions_PhysicsSetVelocity",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022velocity\u0022 (Vector3, defaults to Vector3.zero when omitted).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Velocity set successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., scene object not found or malformed velocity.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/getVelocity": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Read Rigidbody velocity",
        "description": "Returns 200 with the bare array [x, y, z] carrying the current velocity as the response body. The response is not wrapped in an ok/action envelope.\r\nA scene object without a Rigidbody returns 200 with [0, 0, 0] rather than an error.\r\nReturns 422 ACTION_FAILED if no scene object matches the tag, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe returned vector reflects the velocity at the last physics step; values are in world units per second. See POST /api/actions/physics/setVelocity to overwrite the vector.\r\n            \r\n*Example: When a chat overlay polls every few seconds during a stunt segment, read the streamer kart Rigidbody velocity and pipe the magnitude into an on-stream speedometer texture.*",
        "operationId": "Actions_PhysicsGetVelocity",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Velocity retrieved."
          },
          "422": {
            "description": "Action failed - e.g., no scene object matches the tag.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/movePosition": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Move Rigidbody position",
        "description": "Returns 200 with {ok: true, action: \u0022physics.movePosition\u0022} on success.\r\nA scene object without a Rigidbody is left where it is, and the call still returns 200. Returns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nMovePosition interpolates the Rigidbody across the next physics step rather than teleporting, which preserves continuous collision detection and avoids tunnelling. See POST /api/actions/physics/changeRotation for the rotational equivalent.\r\n            \r\n*Example: When a channel point reward picks a named camera mark, move the camera-rig Rigidbody to that mark\u0027s Vector3 so the on-stream framing slides smoothly into place between physics steps.*",
        "operationId": "Actions_PhysicsMovePosition",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022position\u0022 (Vector3, defaults to [0, 0, 0], the world origin).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Position move applied."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/physics/changeRotation": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Rotate Rigidbody orientation",
        "description": "Returns 200 with {ok: true, action: \u0022physics.changeRotation\u0022} on success.\r\nA scene object without a Rigidbody keeps its orientation, and the call still returns 200. Returns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022rotation\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nMoveRotation interpolates the Rigidbody orientation across the next physics step rather than snapping, which keeps the rotation in step with continuous collision detection. See POST /api/actions/physics/movePosition for the positional equivalent.\r\n            \r\n*Example: When a viewer command names a compass heading during a \u0022spin the prop\u0022 segment, rotate the prop Rigidbody to the matching Euler Vector3 so the on-stream object turns smoothly to face the new direction.*",
        "operationId": "Actions_PhysicsChangeRotation",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022rotation\u0022 (Vector3, Euler angles in degrees; defaults to [0, 0, 0], the identity orientation).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotation change applied."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/process/start": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Start a process",
        "description": "Returns 200 with the bare object {processId, processName} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022path\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022window_style\u0022 is not one of its names (nothing is launched) or the executable cannot be located or launch fails, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe call returns as soon as the process is launched; processName in the immediate response is derived from the executable path. Subscribe to process.loaded and process.exited on /api/events to react to lifecycle changes for this processId on the caller\u0027s own clock. See POST /api/actions/process/terminate for the corresponding kill operation.\r\n            \r\n*Example: When a viewer redeems a \u0022spawn the overlay\u0022 channel point reward, launch the companion visualizer process so its window pops up on the stream alongside the main scene.*",
        "operationId": "Actions_ProcessStart",
        "requestBody": {
          "description": "Required dictionary with key \u0022path\u0022 (String). Optional keys \u0022args\u0022 (String), \u0022working_folder\u0022 (String), and \u0022window_style\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Process started."
          },
          "400": {
            "description": "Missing or non-String \u0022path\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown window_style name or executable not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/process/terminate": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Terminate a process",
        "description": "Returns 200 with {ok: true, action: \u0022process.terminate\u0022} on success. The call sends the kill and returns without waiting for the process to exit; process.exited on /api/events reports the exit and its exit code.\r\nReturns 422 ACTION_FAILED if no process launched through POST /api/actions/process/start in this OverMox session matches (a process launched any other way, including by a graph node, is refused), if \u0022processId\u0022 is outside the 32-bit range (-2147483648 to 2147483647) once any fraction is dropped (refused naming \u0022processId\u0022 and that range), if the process has already exited (the message names its exit code; see POST /api/actions/process/status), or if the host denies the kill, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. By \u0022processName\u0022, a running match wins; a name whose started processes have all exited is refused as already exited.\r\nSee POST /api/actions/process/start for the corresponding launch operation and POST /api/actions/process/status for status polling before termination.\r\n            \r\n*Example: When a chat moderator runs a \u0022!killaudio\u0022 command after a VST plugin freezes, terminate the audio host that an earlier POST /api/actions/process/start launched, so the streamer can start it again without restarting the whole stream.*",
        "operationId": "Actions_ProcessTerminate",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022processId\u0022 (Integer) and \u0022processName\u0022 (String). Send \u0022processId\u0022, or \u0022processName\u0022 alone to target by name; when both are sent, \u0022processId\u0022 wins and \u0022processName\u0022 is ignored, and a request with neither answers 422.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kill sent to the process."
          },
          "422": {
            "description": "Action failed - e.g., no process launched through POST /api/actions/process/start in this session matches, a \u0022processId\u0022 outside the 32-bit range, the process has already exited, or the host denies the kill.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/process/wait": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Wait for a process to exit",
        "description": "Returns 200 with the bare object {hasExited, exitCode} when the process exits or the timeout elapses, whichever comes first; after a timeout, hasExited is false and exitCode is 0. A process that already exited answers at once with hasExited true and its exit code; OverMox keeps the exit codes of the 256 most recently exited processes it started. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if no process started by this OverMox session matches, if \u0022processId\u0022 is outside the 32-bit range (-2147483648 to 2147483647) once any fraction is dropped, or if \u0022timeout\u0022 is above 2147483647 once any fraction is dropped, each of the last two refused naming the key and its range before any wait, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. By \u0022processName\u0022, a running match wins; otherwise the started process of that name that exited last answers. The API waits at most 30 seconds for OverMox to answer, counted from before dispatch, so the 25000 default and a timeout comfortably below 30000 get the 200 answer on a process that keeps running, while a timeout from close to 30000 up to 2147483647 on such a process returns 504 OVERMOX_TIMEOUT instead.\r\nUse this when a downstream graph step depends on the process completing; pair with POST /api/actions/process/start for fire-and-wait pipelines.\r\n            \r\n*Example: When a viewer triggers a \u0022render the highlight reel\u0022 reward that spawns an external encoder, wait for the encoder process to exit before swapping the on-stream scene to the playback overlay.*",
        "operationId": "Actions_ProcessWait",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022processId\u0022 (Integer), \u0022processName\u0022 (String), and \u0022timeout\u0022 (Integer, milliseconds; defaults to 25000). Provide either \u0022processId\u0022 by id or \u0022processName\u0022 by name to identify the target process.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Process exited, or the timeout elapsed first (hasExited false)."
          },
          "422": {
            "description": "Action failed, e.g., no process started by this session matches, a \u0022processId\u0022 outside the 32-bit range, or a \u0022timeout\u0022 above 2147483647.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not answer within 30 seconds, e.g., a timeout from close to 30000 up to 2147483647 on a process that keeps running.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/process/status": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Read process status",
        "description": "Returns 200 with the bare object {hasExited, exitCode, processName, processId}. The response is not wrapped in an ok/action envelope.\r\nA process started by this OverMox session keeps its status after it exits: the call answers hasExited true with the real exit code, and hasExited false with exitCode 0 while the process runs. OverMox keeps the exit codes of the 256 most recently exited processes it started. By \u0022processName\u0022, a running match wins; otherwise the started process of that name that exited last answers.\r\nAn unknown process is not an error: when no process started by this OverMox session matches, the call returns 200 with {hasExited: true, exitCode: -1, processName: \u0022\u0022, processId}, where processId is the requested one with any fraction dropped (-1 when only processName was sent). Returns 422 ACTION_FAILED only when \u0022processId\u0022 is outside the 32-bit range (-2147483648 to 2147483647) once any fraction is dropped, which names no process and is refused naming \u0022processId\u0022 and that range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nPair with POST /api/actions/process/start to capture the ID and POST /api/actions/process/terminate to stop the process.\r\n            \r\n*Example: When a chat moderator runs a \u0022!encoderstatus\u0022 command during a long render, sample this each tick and feed hasExited into a Boolean variable so the overlay shows \u0022encoding...\u0022 until the process exits.*",
        "operationId": "Actions_ProcessStatus",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022processId\u0022 (Integer) and \u0022processName\u0022 (String). Provide either \u0022processId\u0022 by id or \u0022processName\u0022 by name to identify the target process.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status retrieved; an exited process answers with its exit code, and a process this session never started with hasExited true and exitCode -1."
          },
          "422": {
            "description": "Only for a \u0022processId\u0022 outside the 32-bit range once any fraction is dropped; every lookup failure answers 200 with hasExited true and exitCode -1.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/project/refresh": {
      "post": {
        "tags": [
          "L2 Project: Actions"
        ],
        "summary": "Refresh the project panel",
        "description": "Call after any file operation that adds, removes, or renames assets outside the controller so the project panel matches disk.\r\nReturns 200 with the bare object {refreshed, itemCount} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if OverMox cannot rebuild the tree, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/delete and the wider /api/actions/assets/* surface for the file operations that warrant a refresh.\r\n            \r\n*Example: When chat triggers a graph that adds new sound clips via POST /api/actions/assets/create, call project.refresh so the new files appear in the project panel.*",
        "operationId": "Actions_ProjectRefresh",
        "requestBody": {
          "description": "An empty dictionary; this action takes no parameters.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project tree refreshed successfully."
          },
          "422": {
            "description": "Action failed on the OverMox side.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/switch": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Switch the active scene",
        "description": "Returns 200 with {scene} echoing the requested scene name on success.\r\nReturns 422 ACTION_FAILED if no project is loaded, if no project scene matches the name, if the switch is refused because the target scene is still resetting (error reason sceneResetting, retryable once the reset finishes), or if the scene resolves but cannot be loaded (error reason sceneLoadFailed, e.g. a missing or corrupt scene file, not retryable); and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/reset for restoring the current scene without switching.\r\n            \r\n*Example: When a viewer redeems a \u0022next chapter\u0022 channel point reward, switch the active scene to the next named segment so the on-stream backdrop, lighting, and props change in lockstep.*",
        "operationId": "Actions_SceneSwitch",
        "requestBody": {
          "description": "Required dictionary with key \u0022scene\u0022 (String, project scene name with or without its file extension).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene switched successfully."
          },
          "422": {
            "description": "Action failed because no project is loaded, no project scene matched the name, the target scene is still resetting (error reason sceneResetting), or the scene could not be loaded (error reason sceneLoadFailed).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/reset": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Reset the scene",
        "description": "Returns 200 with {scene}, the name of the reloaded scene, on success.\r\nReturns 422 ACTION_FAILED if no scene is loaded, if the scene is already resetting (error reason sceneResetting, retryable once the in-progress reset finishes), or if the scene could not be reloaded (error reason sceneLoadFailed, e.g. a missing or corrupt scene file, not retryable); and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/clear for removing scene objects without reloading the scene.\r\n            \r\n*Example: When a moderator runs a \u0022reset the stage\u0022 command between guest segments, reset the scene so spawned props, color tweaks, and parent changes from the prior segment are dropped before the next guest joins.*",
        "operationId": "Actions_SceneReset",
        "requestBody": {
          "description": "No parameters; the action always reloads the currently loaded scene and ignores any keys supplied.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene reset successfully."
          },
          "422": {
            "description": "Action failed because no scene is loaded, the scene is already resetting (error reason sceneResetting), or the scene could not be reloaded (error reason sceneLoadFailed).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/save": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Save the active scene",
        "description": "Returns 200 with {scene} where scene is the name of the active scene that was written to disk.\r\nReturns 422 ACTION_FAILED if no scene is currently loaded, the scene is currently playing, or the save could not complete, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nShattered objects and their pieces are not written to the save, so they are missing after the scene reloads (POST /api/actions/scene/reset, or opening the scene again); the save still answers 200 and gives no warning. An object is shattered once POST /api/actions/shatter/fragment fragments it, POST /api/actions/shatter/demo breaks it or it breaks after POST /api/actions/shatter/activate, or a fade from POST /api/actions/shatter/fade starts on it. Objects under a shattered object are left out with it, its parent saves without it, and an object with Object Type Mesh Root is left out whole once any part breaks or fades.\r\nThis overwrites the currently loaded scene file in place and does not create a new scene. See POST /api/actions/scene/switch to change the active scene and POST /api/actions/scene/clear to empty it.\r\n            \r\n*Example: When a moderator runs a \u0022lock in the stage\u0022 command after arranging props for the next segment, save the active scene so the current object layout persists to disk and survives a reload.*",
        "operationId": "Actions_SceneSave",
        "requestBody": {
          "description": "No parameters required.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene saved successfully."
          },
          "422": {
            "description": "Action failed because no scene is loaded, the scene is playing, or the save could not complete.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/spawn": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Spawn a scene object",
        "description": "Returns 200 with {tag, name, active, components} describing the spawned object on success, where components lists its component type names; source is added as projectAsset on the \u0022asset\u0022 path, and warning when the requested tag was already in use.\r\nReturns 422 ACTION_FAILED if none or more than one of \u0022type\u0022, \u0022sceneObject\u0022 and \u0022asset\u0022 is supplied, if the MenuPath, source tag or asset does not resolve, if the Element fails to load or instantiate, or if \u0022position\u0022, \u0022rotation\u0022 or \u0022scale\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key; and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSupply exactly one of \u0022type\u0022, \u0022sceneObject\u0022, or \u0022asset\u0022; valid \u0022type\u0022 values are the MenuPaths GET /api/discovery/spawnables lists. See POST /api/actions/scene/objects/destroy for the inverse operation.\r\n            \r\n*Example: When a subscriber redeems a \u0022drop a balloon\u0022 reward, spawn the Balloon Bundled Asset at the streamer-cam anchor with a randomized rotation so a fresh prop appears on-stream each time.*",
        "operationId": "Actions_SceneSpawn",
        "requestBody": {
          "description": "Optional keys \u0022type\u0022 (String, built-in Element MenuPath from GET /api/discovery/spawnables such as Simple/Cube), \u0022sceneObject\u0022 (String, existing object tag to duplicate), \u0022asset\u0022 (String, Bundled Asset or imported model name), \u0022position\u0022 (Vector3), \u0022rotation\u0022 (Vector3), \u0022scale\u0022 (Vector3), \u0022name\u0022 (String), \u0022tag\u0022 (String), and \u0022spawnDisabled\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object spawned successfully."
          },
          "422": {
            "description": "Action failed because none or more than one of type, sceneObject and asset was supplied, the MenuPath, source tag or asset did not resolve or could not be spawned, or a position, rotation or scale value was malformed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/clear": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Clear all scene objects",
        "description": "Returns 200 with the bare object {cleared: \u003Ccount\u003E} on success, where cleared is the number of non-matching scene objects removed. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the scene cannot be cleared, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/reset for reloading the scene to its saved state instead of removing every object.\r\n            \r\n*Example: When the streamer ends an interactive \u0022viewer-built stage\u0022 segment, clear the scene so the next chapter starts on a blank canvas without leftover viewer-spawned props.*",
        "operationId": "Actions_SceneClear",
        "requestBody": {
          "description": "Optional key \u0022preserve\u0022 (Array of String, component type names whose carriers are kept unless their parent is removed).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene cleared successfully."
          },
          "422": {
            "description": "Action failed because the scene could not be cleared.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/find": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Find scene objects",
        "description": "Returns 200 with the bare array [\u003Ctag\u003E, ...] of matching scene-object identifiers on success; the array is empty when nothing matches, and carries every scene object in the scene when neither \u0022name\u0022 nor \u0022tag\u0022 is supplied. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if \u0022findBy\u0022 is not one of its names or the filter cannot be evaluated, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/objects/findWithComponent for filtering by attached component type.\r\n            \r\n*Example: When a chat command says \u0022spotlight the trophies\u0022, search by display name so every prop the streamer named \u0022Trophy Case Left\u0022 and \u0022Trophy Case Right\u0022 comes back in one partial-match pass, ready for a follow-up setColor or setActive call.*",
        "operationId": "Actions_SceneObjectsFind",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022name\u0022 (String), \u0022tag\u0022 (String), and \u0022findBy\u0022 (String) as filters.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search completed successfully."
          },
          "422": {
            "description": "Action failed because \u0022findBy\u0022 is not one of its names or the filter could not be evaluated.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/findWithComponent": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Find objects by component",
        "description": "Returns 200 with the bare array [\u003Ctag\u003E, ...] of matching scene-object identifiers on success; the array is empty when no scene object carries the requested component. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED only when the component parameter is missing or empty; an unrecognized component name is not an error and returns 200 with an empty objects array. Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/objects/find for name-or-tag filtering and GET /api/objects/{tag}/components for the inverse lookup.\r\n            \r\n*Example: When a viewer redeems a \u0022show me the noisy props\u0022 reward, find every scene object that has an AudioSource attached so an on-stream overlay can label each candidate before the next sound cue plays.*",
        "operationId": "Actions_SceneObjectsFindWithComponent",
        "requestBody": {
          "description": "Required dictionary with key \u0022component\u0022 (String, fully-qualified or short component type name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search completed successfully."
          },
          "422": {
            "description": "Action failed because the component parameter was missing or empty.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/destroy": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Destroy a scene object",
        "description": "Returns 200 with {ok: true, action: \u0022scene.objects.destroy\u0022} on success.\r\nReturns 422 ACTION_FAILED if the target ItemTag does not resolve to a live scene object, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nDescendants of the destroyed object are also removed. See POST /api/actions/scene/spawn for the inverse operation and POST /api/actions/scene/clear for bulk removal.\r\n            \r\n*Example: When chat raids a \u0022wreck the prop\u0022 donation goal, destroy the trophy scene object so the follow-up shatter effect plays against an empty pedestal on-stream.*",
        "operationId": "Actions_SceneObjectsDestroy",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the target object\u0027s ItemTag).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object destroyed successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/getChild": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Read a child by index",
        "description": "Returns 200 with the bare identifier string of the child scene object as the response body, or the empty string \u0022\u0022 when the index is outside the parent\u0027s child range. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the parent ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. An out-of-range index is not an error.\r\nIndexes follow scene-hierarchy order. See POST /api/actions/scene/objects/getChildCount for valid range bounds.\r\n            \r\n*Example: When an overlay panel iterates the children of an \u0022Announcements\u0022 scene object to render each banner in turn, walk the indices to pull each child for per-banner color and active toggles.*",
        "operationId": "Actions_SceneObjectsGetChild",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, parent ItemTag); optional key \u0022index\u0022 (Integer, zero-based child slot).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Child retrieved successfully."
          },
          "422": {
            "description": "Action failed because the parent ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/getChildCount": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Read child count",
        "description": "Returns 200 with the bare Integer count as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the parent ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nOnly direct children are counted; nested descendants are not included. See POST /api/actions/scene/objects/getChild for index-by-index access.\r\n            \r\n*Example: When a chat command says \u0022how many cards are pinned to the announcement board\u0022, read the child count of the Announcements scene object so the bot can answer with the live total without parsing the full hierarchy.*",
        "operationId": "Actions_SceneObjectsGetChildCount",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the parent ItemTag).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Child count retrieved successfully."
          },
          "422": {
            "description": "Action failed because the parent ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/getParent": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Read parent of an object",
        "description": "Returns 200 with the bare identifier string of the parent scene object as the response body, or the empty string \u0022\u0022 when the object sits at scene root; the root case is a 200, never null and never an error. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the target ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/objects/setParent for changing the parent and POST /api/actions/scene/objects/getChildCount for the inverse cardinality lookup.\r\n            \r\n*Example: When an overlay panel pretty-prints a scene-object trail for the streamer (\u0022Stage -\u003E Lights -\u003E KeyLight\u0022), read each parent in turn so the breadcrumb reflects the live hierarchy.*",
        "operationId": "Actions_SceneObjectsGetParent",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the target ItemTag).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parent retrieved successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setParent": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Set the parent of an object",
        "description": "Returns 200 with {ok: true, action: \u0022scene.objects.setParent\u0022} on success.\r\nReturns 422 ACTION_FAILED if either ItemTag does not resolve or the new parent is a descendant of the object, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nBy default (world_position_stays=true) world-space Transform values are preserved across the reparent; pass false to keep the object\u0027s local Transform and let its world position move to the parent\u0027s frame. See POST /api/actions/scene/objects/getParent for the inverse lookup.\r\n            \r\n*Example: When a viewer drags a chat-spawned prop into the \u0022Stage\u0022 panel, set the prop\u0027s parent to the Stage scene object so subsequent stage-wide moves and color tweaks carry the prop along.*",
        "operationId": "Actions_SceneObjectsSetParent",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the ItemTag to reparent); optional key \u0022parent\u0022 (String, the new parent ItemTag; empty or null detaches to scene root); optional key \u0022world_position_stays\u0022 (Boolean, default true; controls whether world or local Transform values are preserved across the reparent).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parent set successfully."
          },
          "422": {
            "description": "Action failed because the ItemTags did not resolve or the new parent would create a cycle.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setName": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Rename a scene object",
        "description": "Returns 200 with {tag, name, active} reflecting the updated display name on success; the ItemTag is preserved.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe ItemTag remains the stable identifier for all subsequent API calls. See POST /api/actions/scene/objects/setItemTag to change the identifier itself.\r\n            \r\n*Example: When a viewer redeems a \u0022name the mascot\u0022 reward and supplies a chat string, rename the mascot scene object so the new label shows on every overlay panel reading from this object.*",
        "operationId": "Actions_SceneObjectsSetName",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, the persistent ItemTag) and \u0022name\u0022 (String, the new display name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object renamed successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setTag": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Rename the scene object identifier",
        "description": "Returns 200 with {tag, name, active} on success, where \u0022tag\u0022 is the identifier that was actually assigned after normalization and is the only authoritative handle for subsequent calls; the requested value is not stored verbatim.\r\nReturns 422 ACTION_FAILED if the current ItemTag does not resolve or the requested value is empty, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThis call changes the identifier itself, so any stored reference to the previous identifier stops resolving. See POST /api/actions/scene/objects/setName to change the display name instead, which leaves the identifier alone.\r\n            \r\n*Example: When the streamer promotes a prop to a named show fixture, reassign its identifier so every later overlay call addresses it by that stable handle, reading the response tag to learn the exact identifier that was stored.*",
        "operationId": "Actions_SceneObjectsSetTag",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, the current ItemTag) and \u0022tag\u0022 (String, the requested new identifier).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identifier reassigned successfully."
          },
          "422": {
            "description": "Action failed because the current ItemTag did not resolve or the requested value was empty.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setItemTag": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Rename the scene object identifier",
        "description": "Returns 200 with {tag, name, active} on success, where \u0022tag\u0022 is the identifier that was actually assigned after normalization and is the only authoritative handle for subsequent calls; the requested value is not stored verbatim.\r\nReturns 422 ACTION_FAILED if the current ItemTag does not resolve or the requested value is empty, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThis call changes the identifier itself, so any stored reference to the previous identifier stops resolving. See POST /api/actions/scene/objects/setName to change the display name instead, which leaves the identifier alone.\r\n            \r\n*Example: When the streamer promotes a prop to a named show fixture, reassign its identifier so every later overlay call addresses it by that stable handle, reading the response tag to learn the exact identifier that was stored.*",
        "operationId": "Actions_SceneObjectsSetTag",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, the current ItemTag) and \u0022tag\u0022 (String, the requested new identifier).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identifier reassigned successfully."
          },
          "422": {
            "description": "Action failed because the current ItemTag did not resolve or the requested value was empty.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setActive": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Set active state",
        "description": "Returns 200 with {tag, name, active} reflecting the new active state on success.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nDisabling a scene object also disables its children for rendering and physics purposes. See POST /api/actions/scene/objects/destroy for permanent removal.\r\n            \r\n*Example: When a chat poll switches the on-stream backdrop between \u0022day\u0022 and \u0022night\u0022, set active on each backdrop scene object so only the winning variant renders without spawning or destroying any props.*",
        "operationId": "Actions_SceneObjectsSetActive",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the persistent ItemTag); optional key \u0022active\u0022 (Boolean, the desired active state; defaults to false).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Active state set successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/setLayer": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Set object layer",
        "description": "Returns 200 with {tag, name, active, layer} reflecting the applied layer on success.\r\nReturns 422 ACTION_FAILED if the layer name does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe layer name must be a configured OverMox layer. See GET /api/discovery/layers to enumerate valid values and POST /api/actions/scene/objects/getLayer for the inverse lookup.\r\n            \r\n*Example: When a viewer redeems a \u0022hide it\u0022 reward, set a prop\u0027s layer to a non-rendered layer so the scene object drops out of the on-stream camera view without being destroyed.*",
        "operationId": "Actions_SceneObjectsSetLayer",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, the object\u0027s ItemTag) and \u0022layer\u0022 (String, the target layer name).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Layer set successfully. Returns \u0060{\u0022tag\u0022: \u0022...\u0022, \u0022name\u0022: \u0022...\u0022, \u0022active\u0022: true, \u0022layer\u0022: \u0022...\u0022}\u0060."
          },
          "422": {
            "description": "Action failed because the layer name did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/getLayer": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Read object layer",
        "description": "Returns 200 with {tag, layer} on success; layer is an empty string when the layer index has no configured name.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/objects/setLayer for changing the layer and GET /api/discovery/layers for the configured layer list.\r\n            \r\n*Example: When an overlay panel tracks which props sit on the \u0022spotlight\u0022 layer, read each scene object\u0027s layer so the panel can flag the currently highlighted prop for the streamer.*",
        "operationId": "Actions_SceneObjectsGetLayer",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the object\u0027s ItemTag).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Layer read successfully. Returns \u0060{\u0022tag\u0022: \u0022...\u0022, \u0022layer\u0022: \u0022...\u0022}\u0060. Empty string if the layer index has no configured name."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/createLayer": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Create or rename a layer",
        "description": "Returns 200 with {index, name} on success. Pass an empty string as \u0022name\u0022 to clear the layer name at that index, which drops it from the GetLayer, SetLayer, and Other Layer dropdowns; updates are visible on the next refresh.\r\nReturns 400 INVALID_REQUEST if \u0022index\u0022 or \u0022name\u0022 is missing, 422 ACTION_FAILED if the index is reserved or out of range or no project is loaded, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/objects/setLayer for assigning a configured layer to a scene object and GET /api/discovery/layers for the configured layer list.\r\n            \r\n*Example: When the streamer sets up a new \u0022viewer props\u0022 segment, create a dedicated layer so chat-spawned objects can be toggled on and off as a group during the show.*",
        "operationId": "Actions_SceneCreateLayer",
        "requestBody": {
          "description": "Required dictionary with keys \u0022index\u0022 (Integer, target layer index 0-5, 9-15 or 25-31; 6-8 and 16-24 are reserved) and \u0022name\u0022 (String, layer name; pass an empty string to clear the entry at that index).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Layer created, renamed, or cleared. Returns \u0060{\u0022index\u0022: N, \u0022name\u0022: \u0022...\u0022}\u0060."
          },
          "400": {
            "description": "Missing required parameter \u0022index\u0022 or \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid index (reserved or out of range) or no project loaded.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/objects/convert": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Convert a scene object",
        "description": "Returns 200 with {tag, name, converted} returned verbatim (no ok or action wrapper), where tag is the converted object\u0027s ItemTag, name is its display name, and converted is true.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve or no active part of the object (the object itself or an active child) carries a MeshFilter or SkinnedMeshRenderer, so an inactive object is always refused and meshes on inactive children do not satisfy this check, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe action takes no target type: skinned meshes are baked in their current pose, the mesh of every enabled MeshRenderer or SkinnedMeshRenderer on the object and its children is merged into one static mesh on the object (meshes on disabled renderers are dropped), all child objects are removed, and the object keeps its ItemTag. When no enabled renderer carries a mesh, no mesh is merged and no child is removed, and the call still returns converted true. See POST /api/actions/scene/spawn for creating a new object instead.\r\n            \r\n*Example: When a viewer-built stack of props should become one fixed set piece for the rest of the segment, convert the scene object in place so it renders as a single static mesh under the same ItemTag.*",
        "operationId": "Actions_SceneObjectsConvert",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String, the persistent ItemTag).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object converted successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve or no active part of the object (the object itself or an active child) has a MeshFilter or SkinnedMeshRenderer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/setColor": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Set object color",
        "description": "Returns 200 with the bare object {tag, color} on success, where color carries {r, g, b, a}. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 or \u0022color\u0022 is missing, 422 ACTION_FAILED if the ItemTag does not resolve, the object has no Renderer, or the color value is rejected, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nA per-object material instance is created on first call so adjacent objects sharing the same source material keep their original color. See POST /api/actions/scene/objects/convert for merging an object\u0027s meshes into one static mesh.\r\n            \r\n*Example: When chat redeems a \u0022hot pink mode\u0022 reward for a specific prop, set the color on that single scene object so only the targeted prop changes and the rest of the stage stays on-theme.*",
        "operationId": "Actions_SceneSetColor",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, target ItemTag) and \u0022color\u0022 (Color, {r,g,b,a} object, [r,g,b,a] array with a optional, or #RRGGBB / #RRGGBBAA hex string).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Color set successfully."
          },
          "400": {
            "description": "Required parameter \u0022object\u0022 or \u0022color\u0022 was missing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve, the object has no Renderer, or the color value was rejected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/scene/camera/switch": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Switch active camera",
        "description": "Returns 200 with {ok: true, action: \u0022camera.switch\u0022} on success.\r\nReturns 400 INVALID_REQUEST if \u0022camera\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022transition\u0022 is not None or one of the transition names, or if the named Camera scene object does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe previously active Camera is deactivated in the same step. See POST /api/actions/scene/switch for switching the entire scene rather than just the active Camera.\r\n            \r\n*Example: When a chat command says \u0022go to the front cam\u0022, switch the active Camera to FrontStageCam so the on-stream view cuts to the new angle without reloading the scene or respawning props.*",
        "operationId": "Actions_CameraSwitch",
        "requestBody": {
          "description": "Required dictionary with key \u0022camera\u0022 (String, the target Camera scene-object name or identifier); optional key \u0022transition\u0022 (String, transition to play during the switch, or None for an immediate cut).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Camera switched successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022camera\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed because \u0022transition\u0022 is not one of its names or the named Camera did not resolve.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox runtime is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/schema": {
      "get": {
        "tags": [
          "L2 Schema: Actions"
        ],
        "summary": "List all action schemas",
        "description": "Call this without parameters once on startup to cache or prefetch every action schema in a single request, rather than fetching per-action.\r\nEach action schema describes its required and optional parameters, their types, defaults, and an example call.\r\nThe 200 response is polymorphic: the full catalog when action is omitted, and a single ActionParameterSchema (the same shape returned by GET /api/actions/schema/{category}/{actionName}) when the action query parameter is supplied.\r\nReturns 404 ACTION_NOT_FOUND if the supplied action query parameter resolves to no registered schema.\r\nSee GET /api/actions/schema/{category}/{actionName} for a single-action lookup and GET /api/actions/schema/{category} for a category-scoped subset.",
        "operationId": "Schema_GetAllActions",
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "description": "Optional canonical dotted action string (e.g., \u0022scene.spawn\u0022). When supplied, the response narrows to that single action\u0027s parameter schema instead of the full catalog.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns all action schemas, or the single schema matching the action query parameter.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              }
            }
          },
          "404": {
            "description": "No schema found for the specified action query parameter.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/schema/{category}/{actionName}": {
      "get": {
        "tags": [
          "L2 Schema: Actions"
        ],
        "summary": "Get action schema by name",
        "description": "The route segment pair maps to the canonical dotted action string (\u0022scene\u0022 \u002B \u0022spawn\u0022 resolves to action \u0022scene.spawn\u0022).\r\nReturns 404 ACTION_NOT_FOUND if no schema is registered for the resolved action string.\r\nSee GET /api/actions/schema for the full catalog and GET /api/actions/schema/{category}/{*actionPath} for slash-separated sub-category lookups.",
        "operationId": "Schema_GetAction",
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "description": "The action category (e.g., \u0022scene\u0022, \u0022transform\u0022, \u0022physics\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actionName",
            "in": "path",
            "description": "The action name within the category (e.g., \u0022spawn\u0022, \u0022set\u0022, \u0022addForce\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the action\u0027s parameter schema.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              }
            }
          },
          "404": {
            "description": "No schema found for the specified action.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/schema/{category}/{actionPath}": {
      "get": {
        "tags": [
          "L2 Schema: Actions"
        ],
        "summary": "Get nested action schema",
        "description": "Normalizes slash-separated sub-category paths to dot-separated action strings (\u0022scene\u0022 \u002B \u0022objects/setName\u0022 resolves to action \u0022scene.objects.setName\u0022).\r\nUse this variant for nested sub-category actions where the canonical form has multiple dots after the leading category.\r\nReturns 404 ACTION_NOT_FOUND if no schema is registered for the resolved action string.\r\nSee GET /api/actions/schema/{category}/{actionName} for flat two-segment actions and GET /api/actions/schema for the full catalog.",
        "operationId": "Schema_GetActionByPath",
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "description": "The action category (e.g., \u0022scene\u0022, \u0022transform\u0022, \u0022physics\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actionPath",
            "in": "path",
            "description": "The slash-separated action path within the category (e.g., \u0022objects/setName\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns the action\u0027s parameter schema.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionParameterSchema"
                }
              }
            }
          },
          "404": {
            "description": "No schema found for the specified action.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/schema/{category}": {
      "get": {
        "tags": [
          "L2 Schema: Actions"
        ],
        "summary": "Get action schemas by category",
        "operationId": "Schema_GetCategoryActions",
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "description": "The action category (e.g., \u0022scene\u0022, \u0022transform\u0022, \u0022math\u0022).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns all action schemas in the category.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/CategorySchemaResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategorySchemaResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategorySchemaResponse"
                }
              }
            }
          },
          "404": {
            "description": "No actions found in the specified category.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/init": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Initialize the shatter system",
        "description": "Returns 200 with the bare object {tag, initialized} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved, it carries no mesh to break apart, it carries its mesh only on a SkinnedMeshRenderer, it is a Mesh Root object none of whose child objects carries a mesh or none of whose child meshes sits on a MeshFilter, its physics shatter body is gone, or the setup fails, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe shatter component is added when missing. Before the Object Type is settled, a 3D text or shadow box object (one carrying the TextElement3d or DynamicShadowBoxModeler component) is converted in place, with no confirmation and no undo, into one static mesh: those components (and any Animator or Animation component) are removed, the meshes of the object and its child objects are combined into one mesh on the object, and every child object is destroyed, so it is set up as one Object Type Mesh body.\r\nThe setup then settles the object\u0027s Object Type (a shatter component setting) as POST /api/actions/shatter/demo does: Object Type Mesh on an object with no renderer and with child objects becomes Mesh Root, and Mesh Root on an object with no child objects becomes Mesh. On a Mesh Root object each active child object at any depth that carries a mesh is set up as a piece: a piece whose mesh sits on a MeshFilter gets its own physics body, and a piece whose mesh sits on a SkinnedMeshRenderer is left out of the simulation with no Rigidbody, so it stays where it is.\r\nAn Object Type Mesh object that carries no mesh cannot be set up: one with no renderer and no MeshFilter mesh (such as an empty object with no child objects), or one whose renderer holds no mesh (a MeshRenderer with no MeshFilter mesh, or a particle, sprite, line or trail renderer); a mesh of two vertices or fewer counts as no mesh. The call is then refused with 422 before any setup runs, naming GET /api/objects/{tag}/components for checking whether the object carries a mesh.\r\nA Mesh Root object not set up yet none of whose child objects carries a mesh is refused with 422 before any setup runs too, naming POST /api/actions/scene/objects/getChildCount and POST /api/actions/scene/objects/getChild for checking the children; once a child that carries a mesh is added, the next init sets the object up.\r\nA mesh that sits on a SkinnedMeshRenderer is left out of the simulation, so an object whose every mesh sits on one cannot be set up: an Object Type Mesh object whose mesh sits only on a SkinnedMeshRenderer (it has no MeshFilter mesh), and a Mesh Root object not set up yet none of whose child meshes sits on a MeshFilter. The call is then refused with 422 before any setup runs, naming POST /api/actions/shatter/demo, which breaks such an object apart without a physics body.\r\nWhen the object\u0027s physics shatter body is gone (a fade with the MeshShatter fadeType SimExclude removes it), nothing is set up and the call is refused with 422, naming POST /api/actions/components/remove for removing the shatter component, after which the next init adds a fresh one with default settings.\r\nA failure inside the setup answers 422 ACTION_FAILED naming the error. The shatter component stays on the object after any refusal.\r\nInit applies the shatter component\u0027s Simulation Type to the body. With the default Simulation Type of Dynamic, the body becomes dynamic under gravity at init, so the object falls right away; set Simulation Type to Inactive or Kinematic before this call to hold the object in place until POST /api/actions/shatter/activate.\r\nThe Initialize button of the MeshShatter component runs the same setup, asking for confirmation before that conversion: writing true to that component property through PUT /api/objects/{tag}/components/{component}/{property} presses it, and writing false does nothing.\r\nSee POST /api/actions/shatter/activate to fire the prepared object.\r\n            \r\n*Example: When a streamer queues up a \u0022destroy the trophy\u0022 channel point reward at the top of the segment, initialize the shatter system on the trophy whose shatter component holds it Inactive, so the trophy stays in place until the reward redemption later in the show activates it to fall under physics and break apart on impact.*",
        "operationId": "Actions_ShatterInit",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shatter initialized."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., the target scene object cannot be resolved, it carries no mesh to break apart, it carries its mesh only on a SkinnedMeshRenderer, none of its child objects carries a mesh, its physics shatter body is gone, or the setup fails.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/activate": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Activate the shatter effect",
        "description": "Returns 200 with the bare object {tag, activated} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved, has no shatter component yet (the message names POST /api/actions/shatter/init as the call to make first and GET /api/objects/{tag}/components to check for the MeshShatter component), carries no mesh to break apart, carries its mesh only on a SkinnedMeshRenderer, is a Mesh Root object with no child piece left that can become dynamic, has a physics shatter body that is gone or has no Rigidbody, or the activation fails, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe activation settles the object\u0027s Object Type as POST /api/actions/shatter/init does, and sets up a physics body that init has not set up yet before activating it. It refuses with 422, before anything is set up or activated, what init refuses: an Object Type Mesh object that carries no mesh (naming GET /api/objects/{tag}/components) or whose mesh sits only on a SkinnedMeshRenderer, which the simulation leaves out (naming POST /api/actions/shatter/demo), a Mesh Root object not set up yet none of whose child objects carries a mesh or none of whose child meshes sits on a MeshFilter, and a physics shatter body that is gone (a fade with the MeshShatter fadeType SimExclude removes it; the message names POST /api/actions/components/remove, then POST /api/actions/shatter/init).\r\nA physics shatter body with no Rigidbody cannot become dynamic: the shatter component\u0027s Simulation Type Static sets up none, and a fade with the fadeType SetStatic or removing the Rigidbody component takes it away. The call is then refused with 422 before anything is activated, naming POST /api/actions/components/remove for removing the shatter component and POST /api/actions/shatter/init for a fresh setup with default settings (Simulation Type Dynamic).\r\nOn an object whose meshes sit only on child objects (Object Type Mesh Root), each child piece that is active, not yet demolished and has a Rigidbody becomes dynamic instead; a piece whose mesh sits on a SkinnedMeshRenderer (left out of the simulation) or that has no Rigidbody stays as it is. With no piece that can become dynamic nothing is activated and the call is refused with 422: with no live piece left (no child carries a mesh, or every piece is gone or demolished) the message names POST /api/actions/scene/objects/getChildCount and POST /api/actions/scene/objects/getChild for checking the children; when the mesh of every live piece sits on a SkinnedMeshRenderer it names POST /api/actions/shatter/demo; otherwise (the live pieces have no Rigidbody: the shatter component\u0027s Simulation Type Static sets up none, and a fade with the fadeType SetStatic or removing the Rigidbody component takes it away) it names POST /api/actions/components/remove, then POST /api/actions/shatter/init.\r\nA failure inside the activation answers 422 ACTION_FAILED naming the error. The shatter component stays on the object after any refusal.\r\nWith the shatter component\u0027s default Simulation Type of Dynamic, the body is already dynamic under gravity after POST /api/actions/shatter/init, so the object falls before this call; set Simulation Type to Inactive or Kinematic before POST /api/actions/shatter/init to hold the object in place until activation.\r\nThe Activate button of the MeshShatter component runs the same activation: writing true to that component property through PUT /api/objects/{tag}/components/{component}/{property} presses it, and writing false does nothing.\r\nActivation alone leaves scene saves unchanged, but once the activated object breaks apart on impact it is left out of scene saves, so POST /api/actions/scene/save no longer writes it or any child object still under it; on a Mesh Root object the root and every child piece are left out once any child piece breaks.\r\nSee POST /api/actions/shatter/fade to fade the activated object out and POST /api/actions/shatter/demo to demolish an object into fragments in one call without a prior init.\r\n            \r\n*Example: When a donation crosses the \u0022drop the vase\u0022 tier on screen, activate the vase whose shatter component holds it Inactive so the on-stream prop falls under physics and can break apart on impact.*",
        "operationId": "Actions_ShatterActivate",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shatter activated."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., the target scene object cannot be resolved, has no shatter component, carries no mesh or only a SkinnedMeshRenderer mesh, has no child piece left that can become dynamic, its physics shatter body is gone or has no Rigidbody, or the activation fails.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/fade": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Start a shatter fade",
        "description": "Returns 200 with the bare object {tag, fading} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved, has no shatter component yet (the message names POST /api/actions/shatter/init as the call to make first and GET /api/objects/{tag}/components to check for the MeshShatter component), or its fade fails to start, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe fade settles the object\u0027s Object Type as POST /api/actions/shatter/init does, and a body not set up yet is set up first unless the fade Type is None or the object is inactive (a body that gets a Rigidbody then falls under gravity with the default Simulation Type of Dynamic). On an object whose meshes sit only on child objects (Object Type Mesh Root), each child piece that is active and not yet demolished fades, first taking the shatter component\u0027s current Fading settings (a child object carrying its own shatter component keeps its own settings), so a Fading change made after the object was set up reaches its pieces; unless the fade Type is None, such an object none of whose child objects carries a mesh, or with no child piece left (every piece is gone or demolished), fails to start its fade.\r\nA fade that fails to start answers 422 ACTION_FAILED naming the error, POST /api/actions/shatter/init as the call to retry after, and GET /api/objects/{tag}/components/MeshShatter for checking the fade settings. One such failure is a physics shatter body that is gone (a fade with the MeshShatter fadeType SimExclude removes it). POST /api/actions/shatter/init refuses that body too, so to fade the object again call POST /api/actions/components/remove with component MeshShatter on it, then POST /api/actions/shatter/init, which adds a fresh shatter component with default settings; set its fadeType again before the next fade, since the default None fades nothing.\r\nFading follows the shatter component\u0027s Fading settings: the body lives for its Life Time plus a random Life Variation (counted from when the body comes to rest if the Life Type is By Simulation And Life Time), then its fade Type applies, with Fall Down, Move Down and Scale Down running over Fade Time and Sim Exclude, Destroy, Set Static and Set Kinematic acting at once. With the default fade Type of None, or on an inactive object, the call sets nothing up and starts no fade; on a body larger than a nonzero Size Filter, or on a body whose fade has already started (living, fading or faded), no fade starts, though a body not set up yet is still set up first. Each such call still returns 200. An object a demolish broke apart is hidden at once and destroyed one second later, after which the call returns 422 ACTION_FAILED because the object cannot be resolved. Fragments from POST /api/actions/shatter/demo fade through the component\u0027s On Demolition fading setting, not through this call. See POST /api/actions/shatter/activate to make the body dynamic first.\r\nOnce the fade Type applies, the object is left out of scene saves whatever that Type is, Set Static and Set Kinematic included, so POST /api/actions/scene/save no longer writes it or any child object still under it; while the body still lives out its Life Time it saves as before, and with the default fade Type of None it keeps saving. On an object whose meshes sit only on child objects (Object Type Mesh Root), the root and every child piece are left out once any child piece starts fading.\r\n            \r\n*Example: When a celebration alert finishes playing after a \u0022knock it over\u0022 subscriber reward, fade the activated prop so it clears from the stream before the next segment begins.*",
        "operationId": "Actions_ShatterFade",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fade started."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., the target scene object cannot be resolved, has no shatter component, or its fade fails to start.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/demo": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Run a shatter demo",
        "description": "Returns 200 with the bare object {tag, demolished} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved, it carries no mesh to break apart, it is a Mesh Root object and none of its child pieces is left to demolish, its physics shatter body is gone, or the demolish fails, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe endpoint runs one demolish: it adds the shatter component when missing and breaks the object into fragments. Before the Object Type is settled, a 3D text or shadow box object (one carrying the TextElement3d or DynamicShadowBoxModeler component) is converted in place, with no confirmation and no undo, into one static mesh: those components (and any Animator or Animation component) are removed, the meshes of the object and its child objects are combined into one mesh on the object, and every child object is destroyed, so it breaks apart as one Object Type Mesh body.\r\nThe demolish then settles the object\u0027s Object Type (a shatter component setting): Object Type Mesh on an object with no renderer and with child objects becomes Mesh Root, and Mesh Root on an object with no child objects becomes Mesh.\r\nOn a Mesh Root object it breaks apart the meshes of its child objects, not its own: it demolishes each child piece that is active and not yet demolished; with no such piece left (no child carries a mesh, or every piece is already demolished) nothing breaks apart and the call is refused with 422, naming POST /api/actions/scene/objects/getChildCount and POST /api/actions/scene/objects/getChild for checking the children.\r\nOn a Mesh object it breaks apart only the object\u0027s own mesh of three or more vertices: its MeshFilter mesh (shown by a MeshRenderer, or with no renderer at all) or the mesh of its SkinnedMeshRenderer. An object that carries no such mesh cannot be demolished: one with no renderer and no MeshFilter mesh (such as an empty object with no child objects), or one whose renderer holds no mesh (a MeshRenderer with no MeshFilter mesh, or a particle, sprite, line or trail renderer), with or without child objects. The call is then refused with 422 before any demolish runs, naming GET /api/objects/{tag}/components for checking whether the object carries a mesh.\r\nWhen the object\u0027s physics shatter body is gone (a fade with the MeshShatter fadeType SimExclude removes it), nothing breaks apart and the call is refused with 422, naming POST /api/actions/components/remove for removing the shatter component, after which the next demo adds a fresh one with default settings.\r\nA failure inside the demolish answers 422 ACTION_FAILED naming the error. The shatter component stays on the object after any refusal.\r\nThe Demolish button of the MeshShatter component runs the same demolish, asking for confirmation before that conversion: writing true to that component property through PUT /api/objects/{tag}/components/{component}/{property} presses it, and writing false does nothing.\r\nOnce the object breaks apart, it and its fragments are left out of scene saves, so POST /api/actions/scene/save writes neither, nor any child object still under the object; on a Mesh Root object the root and every child piece are left out once any child piece breaks.\r\nSee POST /api/actions/shatter/init, POST /api/actions/shatter/activate, and POST /api/actions/shatter/fade for the staged init, activate, and fade path.\r\n            \r\n*Example: When a raid lands and the streamer wants an instant on-stream \u0022boom\u0022 without prearming anything, run the demo on a stand-in prop so the raid moment doubles as a one-shot destruction beat.*",
        "operationId": "Actions_ShatterDemo",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Demolish ran."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., the target scene object cannot be resolved, it carries no mesh to break apart, none of its child pieces is left to demolish, its physics shatter body is gone, or the demolish fails.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/fragment": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Fragment an object into static pieces",
        "description": "Returns 200 with the bare object {tag, fragmented} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe request reads no key besides \u0022object\u0022: piece count and cut pattern come from the shatter component\u0027s own settings for the object\u0027s own mesh and from default settings for meshes on child objects, never from the call.\r\nNo prior POST /api/actions/shatter/init is needed. The shatter component is added when missing and switched to static mode, which drops its physics-mode settings but keeps any Rigidbody that POST /api/actions/shatter/init added (on the object, or on each child piece of a Mesh Root), so an object initialized first keeps its physics body. A repeat call on an object whose own mesh already holds fragments changes nothing while the scene stays loaded and no init, activate, fade or demo call has switched the component to physics mode since; on an object whose meshes sit only on child objects, each call fragments the object again, earlier fragments included.\r\nOnce fragmented, the object and its fragments are left out of scene saves, so POST /api/actions/scene/save writes neither, nor any child object still under the object; undoing the fragment makes the object save again.\r\nSee POST /api/actions/shatter/scale to resize the resulting pieces.\r\n            \r\n*Example: When a viewer redeems a \u0022crack the statue\u0022 channel point reward, fragment the statue into static pieces so the on-stream prop breaks apart where it stands.*",
        "operationId": "Actions_ShatterFragment",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object fragmented."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - the target scene object cannot be resolved.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/shatter/scale": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Scale shatter fragments",
        "description": "Returns 200 with the bare object {tag, scale} on success, where scale echoes the applied value. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the target scene object cannot be resolved or has no shatter component yet (the message names POST /api/actions/shatter/fragment as the call to make first and GET /api/objects/{tag}/components to check for the MeshShatter component), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe value is set on every renderer on an active object under the scene object other than the object\u0027s own (renderers on inactive objects keep their scale), which after POST /api/actions/shatter/fragment includes the static fragments; an inactive object, or one with no active child renderers, is a no-op that still returns 200, and on an unfragmented object with child meshes the value resizes those child meshes. The call also switches the shatter component to static mode, as fragment does.\r\n            \r\n*Example: When a moderator says \u0022make those shards big\u0022 during a destruction segment, scale the fragments up so the on-stream debris reads clearly to viewers watching from a small chat window.*",
        "operationId": "Actions_ShatterScale",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022scale\u0022 (Float, uniform scale for every fragment, default 1).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scale applied to fragments."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - the target scene object cannot be resolved or has no shatter component.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/streamdeck/update-button": {
      "post": {
        "tags": [
          "L2 Streamdeck: Actions"
        ],
        "summary": "Update Stream Deck button",
        "description": "Fire-and-forget: sends an editButton message to every Stream Deck plugin client connected to OverMox and returns without waiting for or reading a reply, so sent does not confirm that a device applied the update, and a call with no plugin connected still returns sent true. Only the keys present in the request change: the title is applied when \u0022title\u0022 is present and the image when \u0022imagePath\u0022 is present. The message travels to the plugin as UTF-8 text, so a title or image path with non-ASCII characters (accented letters, CJK, emoji) arrives intact.\r\nReturns 200 with the bare object {sent, title} on success, where sent is always true and title echoes the requested label, an empty string when \u0022title\u0022 was omitted. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022nodeId\u0022, \u0022title\u0022 or \u0022imagePath\u0022 is present but not a String, 422 ACTION_FAILED only if OverMox cannot hand the message to its Stream Deck connection, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. A missing button mapping or a device that ignores the update is not reported.\r\nPOST /api/actions/streamdeck/update-button is the only Stream Deck action.\r\n            \r\n*Example: When the stream goes live, call streamdeck.updateButton to light up a \u0022Live\u0022 button on the Stream Deck so the streamer sees current status at a glance.*",
        "operationId": "Actions_StreamdeckUpdateButton",
        "requestBody": {
          "description": "Optional keys \u0022nodeId\u0022 (String), \u0022title\u0022 (String), and \u0022imagePath\u0022 (String). Provide \u0022nodeId\u0022 to identify the target button mapping.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update message sent to the Stream Deck connection."
          },
          "400": {
            "description": "A present nodeId, title or imagePath is not a String (INVALID_REQUEST).",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed on the OverMox side; for example, the message could not be handed to the Stream Deck connection.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/streaming/platforms": {
      "post": {
        "tags": [
          "L2 Streaming: Actions"
        ],
        "summary": "List streaming platforms",
        "description": "Returns 200 with the bare object {platforms} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the platform registry cannot be enumerated, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nUse the platform name and an eventType entry as inputs to POST /api/actions/streaming/subscribe. Both platforms report authMethod token, since every subscription carries a pasted token.\r\nThe streamelements eventTypes are, in this order: all, follow, sub, communityGift, cheer, tip, raid, superchat, membership, channelPoints, hypeTrain, chat, streamStatus and counter. The streamlabs eventTypes are donation, follow, sub, cheer, raid, ytSub, ytSuperChat and all.\r\nEach entry reports connectionState across that platform\u0027s subscriptions: error when any subscription is in error, otherwise connected when any is connected, then connecting when any is connecting, and otherwise disconnected, as it is with no subscription on that platform. connectionError carries the reason of a subscription in error and is null in every other state; it never contains the token. A streamelements reason is a bare code (token_rejected, scope_missing, network or gateway_refused) and a streamlabs reason is short text; POST /api/actions/streaming/status reports the state of each subscription.\r\nconnected is true exactly when connectionState is connected, so it is false with no subscription on that platform, while connecting, and when any of its subscriptions is in error, such as a refused token.\r\n            \r\n*Example: When a streamer opens an integrations panel before going live, populate the platform picker with connected status flags so dead integrations stand out before the show starts.*",
        "operationId": "Actions_StreamingPlatforms",
        "requestBody": {
          "description": "Empty dictionary; the endpoint takes no inputs but the action wrapper requires the body.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Platform list returned."
          },
          "422": {
            "description": "Action failed - e.g., the platform registry cannot be enumerated.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/streaming/subscribe": {
      "post": {
        "tags": [
          "L2 Streaming: Actions"
        ],
        "summary": "Subscribe to a platform event",
        "description": "Returns 200 with the bare object {subscriptionId, platform, eventType, subscribedAt} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022platform\u0022 or \u0022eventType\u0022 is missing or any of these keys is sent with a value that is not a String, 422 ACTION_FAILED if the platform or eventType is empty or unrecognized (an eventType the platform does not deliver included, with a message listing the types it does) or the token is missing or blank, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nA streamelements request is refused with 422 ACTION_FAILED at the first of these checks it fails, in this order: an unknown platform, an unknown eventType, a sourcePlatform other than any, twitch, youtube or kick, a missing or blank token, and a tokenType other than jwt or apikey. Each refusal names its key (\u0027sourcePlatform\u0027, \u0027token\u0027 or \u0027tokenType\u0027 for the last three) and never contains the token.\r\nSubscribed events are broadcast as \u0022streaming.event\u0022 L3 messages on the WebSocket hub, rate-limited to 100 events per second across all streaming subscriptions; events past the limit in a given second are dropped. Every streaming.event data object carries subscriptionId, platform and eventType of the delivering subscription, plus timestamp (the UTC delivery time as an ISO 8601 round-trip string), beside the event\u0027s own fields such as username, message and raw. See POST /api/actions/streaming/platforms for valid platform and eventType pairs, and POST /api/actions/streaming/unsubscribe to stop delivery.\r\nA streamlabs subscription with eventType all delivers each Streamlabs event exactly once, and its raw field is always the raw Streamlabs socket payload as a JSON string, whether or not the event type is recognized. A streamlabs subscription succeeds before the socket connects; a refused token shows up as connectionState error in POST /api/actions/streaming/status.\r\nA streamelements subscription runs on the one StreamElements gateway connection OverMox keeps: subscriptions with the same token share its channel, and different tokens run their channels side by side. It succeeds before the gateway answers; a refused token shows up as connectionState error with connectionError token_rejected in POST /api/actions/streaming/status. The Overlay Token reaches every eventType: its tip subscriptions read tip activities, with no moderation hold. The JWT adds the tips feed, which holds a tip until moderation allows it and drops a rejected one; each tip is still delivered once. YouTube chat and events need a public stream: StreamElements cannot read a private or unlisted YouTube stream, so none of its chat or events reach a subscription.\r\nEach streamelements delivery carries its eventType\u0027s fields in this order, then isTest (every eventType but chat, streamStatus and counter), then sourcePlatform, then raw: all has event (the activity type, such as follow, subscriber, tip or cheer), username, displayName, amount and message; follow has username and displayName; sub has username, displayName, months, streak, tier, gifted, sender and message; communityGift has sender, displayName, count and tier; cheer has username, displayName, bits and message; tip has username, amount, currency and message; raid has username, displayName and viewers; superchat has username, displayName, amount, currency and message; membership has username, displayName, tier, months, gifted, sender, count and message; channelPoints has username, displayName, reward and message; hypeTrain has stage, level, progress and total; chat has username, displayName, userId, message, color, badges and messageId; streamStatus has isLive; counter has counter and value.\r\nmonths, streak, bits, count, viewers, level, total and value are Integers; amount and progress are Floats; isTest, gifted and isLive are Booleans; every other field is a String. A field the message does not carry reads as an empty String, 0 or false. isTest is true for an alert emulated from the StreamElements dashboard (a replayed alert included) and false for a live one. sourcePlatform is twitch, youtube or kick, or empty when the event carries no platform. raw is the whole gateway message as a JSON string, with a donor\u0027s email and location removed. An all subscription delivers every activity and tip once, types without an eventType of their own included, keeps eventType all and names the activity type in event; the sub eventType covers subscribers on every platform.\r\n            \r\n*Example: When a viewer tips, a tip subscription delivers the amount so a graph grows an on-stream goal bar, skipping deliveries whose isTest is true.*",
        "operationId": "Actions_StreamingSubscribe",
        "requestBody": {
          "description": "Required dictionary with keys \u0022platform\u0022 (String) and \u0022eventType\u0022 (String). Optional keys \u0022token\u0022 (String), \u0022tokenType\u0022 (String) and \u0022sourcePlatform\u0022 (String); every platform refuses a request without a token.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription created. Returns subscriptionId, platform, eventType and subscribedAt."
          },
          "400": {
            "description": "Missing or non-String \u0022platform\u0022 or \u0022eventType\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., unknown platform, invalid event type or missing token.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/streaming/unsubscribe": {
      "post": {
        "tags": [
          "L2 Streaming: Actions"
        ],
        "summary": "Cancel a platform subscription",
        "description": "Returns 200 with the bare object {removed} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the subscriptionId is unknown or already released, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe subscriptionId comes from the response of POST /api/actions/streaming/subscribe. See POST /api/actions/streaming/status to list currently active subscriptions.\r\n            \r\n*Example: When a streamer ends a \u0022donations drive overlays\u0022 segment, release the donation subscription so the next segment\u0027s graphs do not keep firing on lingering events.*",
        "operationId": "Actions_StreamingUnsubscribe",
        "requestBody": {
          "description": "Required dictionary with key \u0022subscriptionId\u0022 (String) from the original subscribe response.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription removed."
          },
          "422": {
            "description": "Action failed - e.g., invalid subscription ID.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/streaming/status": {
      "post": {
        "tags": [
          "L2 Streaming: Actions"
        ],
        "summary": "List active subscriptions",
        "description": "Returns 200 with the bare object {subscriptions} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the subscription registry cannot be enumerated, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/streaming/subscribe to add a subscription and POST /api/actions/streaming/unsubscribe to remove one by ID.\r\nEach subscription reports its own connectionState (connecting, connected, disconnected or error): a streamlabs subscription for its own socket, a streamelements subscription for its topics on the shared StreamElements gateway connection. connectionError carries the reason when the state is error and is null in every other state; it never contains the token. A streamlabs reason is short text, such as a socket token Streamlabs refused.\r\nA streamelements reason is a bare code: token_rejected (StreamElements refused the token on every topic; it is never retried, so unsubscribe and subscribe again with a fresh token or the Overlay Token), scope_missing (the token lacks the scope for the topics the eventType needs), network (the connection dropped or could not connect and is retrying with backoff) or gateway_refused (the gateway refused the subscription for another reason). A streamelements subscription reads connecting until one of its topics is live and connected while any of them is, so a tip subscription on the Overlay Token, which has no tips feed, reads connected through its activity feed.\r\n            \r\n*Example: When a streamer suspects a stale integration is firing duplicate alerts, list active subscriptions to spot two donation subscribers on the same platform before unsubscribing the older one.*",
        "operationId": "Actions_StreamingStatus",
        "requestBody": {
          "description": "Empty dictionary; the endpoint takes no inputs but the action wrapper requires the body.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Active subscriptions returned."
          },
          "422": {
            "description": "Action failed - e.g., the subscription registry cannot be enumerated.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/text/format": {
      "post": {
        "tags": [
          "L2 Text: Actions"
        ],
        "summary": "Format a string",
        "description": "Returns 200 with the bare formatted string as the response body. The response is not wrapped in an ok/action envelope.\r\nPlaceholders in the template use {name} syntax. Each one takes the value of the graph variable with that name; only when no graph variable matches does it fall back to the \u0022values\u0022 entry of that name, and then to a top-level key of that name. A placeholder that resolves to none of these becomes an empty string.\r\nReturns 400 INVALID_REQUEST if \u0022template\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022template\u0022 is an empty String, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/text/operation for non-template String transforms.\r\n            \r\n*Example: When a viewer redeems a \u0022name on screen\u0022 reward, format \u0022Welcome {name} to the stream\u0022 with their handle and feed the result into a String variable driving an on-stream caption.*",
        "operationId": "Actions_TextFormat",
        "requestBody": {
          "description": "Required dictionary with key \u0022template\u0022 (String). Optional key \u0022values\u0022 (Object, placeholder name to replacement value, used only for a placeholder that names no graph variable).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "String formatted."
          },
          "400": {
            "description": "Missing or non-String \u0022template\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - \u0022template\u0022 is an empty String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/text/parse": {
      "post": {
        "tags": [
          "L2 Text: Actions"
        ],
        "summary": "Parse a string",
        "description": "Returns 200 with the bare parsed result as the response body. The response is not wrapped in an ok/action envelope.\r\nThe shape of the result depends on \u0022type\u0022: split returns an Array of String; regex returns an Array of String with one entry per match (capture group 1 when the pattern has a group, otherwise the whole match); json returns the value at the property path as a String (JSON text for an object or array); xml returns the inner XML of the element at the property path as a String. With no \u0022property\u0022, json mode returns the input text unchanged when it parses as a JSON object, and xml mode when it parses as XML.\r\nInput that is not a JSON object in json mode (a top-level JSON array or scalar included) or not valid XML in xml mode, or a property path that matches nothing, returns 200 with an empty string rather than an error.\r\nReturns 400 INVALID_REQUEST if \u0022text\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022text\u0022 is an empty String, \u0022type\u0022 is not one of its names, or, in regex mode, the pattern is not a valid regular expression, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/text/operation for token-level String manipulations.\r\n            \r\n*Example: When a chat moderator sets a cue list such as \u0022intro,music,lights\u0022, parse it in split mode and feed the resulting Array into a For Each Array Item node so each entry fires its own scene cue.*",
        "operationId": "Actions_TextParse",
        "requestBody": {
          "description": "Required dictionary with key \u0022text\u0022 (String). Optional keys \u0022pattern\u0022 (String), \u0022type\u0022 (String, one of \u0022split\u0022, \u0022regex\u0022, \u0022json\u0022, \u0022xml\u0022), and \u0022property\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Text parsed."
          },
          "400": {
            "description": "Missing or non-String \u0022text\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - invalid regex pattern, unknown type name, or empty \u0022text\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/text/operation": {
      "post": {
        "tags": [
          "L2 Text: Actions"
        ],
        "summary": "Run a string operation",
        "description": "Returns 200 with the bare operation result as the response body. The response is not wrapped in an ok/action envelope.\r\nThe result type follows the operation: Length and IndexOf return a bare Integer, Contains returns a bare Boolean, Split returns the object {result, result2}, and the other operations return a bare String.\r\nAn operand key the operation reads but the request omits falls back to a default (0 for an index, an empty string for text, the rest of the input for \u0022length\u0022) rather than refusing, so Remove with \u0022string2\u0022 omitted or empty strips nothing and returns the input unchanged.\r\nReturns 400 INVALID_REQUEST if \u0022input\u0022 or \u0022type\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022type\u0022 names no listed operation, \u0022input\u0022 is an empty String, or SubString gets a negative \u0022length\u0022 with \u0022index_start\u0022 inside the input, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/text/format for template substitution and POST /api/actions/text/parse for structured extraction.\r\n            \r\n*Example: When a donation message arrives, run ToUpper on the donor name and Append \u0022 JUST TIPPED\u0022 to populate a String variable driving a flashing on-stream banner.*",
        "operationId": "Actions_TextOperation",
        "requestBody": {
          "description": "Required dictionary with keys \u0022input\u0022 (String) and \u0022type\u0022 (String, one of \u0022GetCharacter\u0022, \u0022ToLower\u0022, \u0022ToUpper\u0022, \u0022Insert\u0022, \u0022Append\u0022, \u0022Remove\u0022, \u0022RemoveAt\u0022, \u0022Replace\u0022, \u0022Join\u0022, \u0022Split\u0022, \u0022SubString\u0022, \u0022Contains\u0022, \u0022IndexOf\u0022, \u0022Length\u0022). Optional keys \u0022index\u0022 (Integer), \u0022string2\u0022 (String), \u0022value\u0022 (String), \u0022old\u0022 (String), \u0022new\u0022 (String), \u0022index_start\u0022 (Integer), and \u0022length\u0022 (Integer). See per-key documentation for which operations consume each optional key.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Operation completed."
          },
          "400": {
            "description": "Missing or non-String \u0022input\u0022 or \u0022type\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - unknown operation name, empty \u0022input\u0022, or a negative SubString \u0022length\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/get": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Read transform values",
        "description": "Returns 200 with the bare object {position, rotation, scale} on success, where rotation is Euler degrees. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022space\u0022 is not one of its names, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/set to write back the same channels and POST /api/actions/transform/getDirection for axis-derived vectors.\r\n            \r\n*Example: When a viewer asks \u0022where is the trophy\u0022, read the trophy scene object\u0027s world position and echo the numbers into an on-stream caption so chat can debate the next placement.*",
        "operationId": "Actions_TransformGet",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022space\u0022 (String, one of \u0022world\u0022 or \u0022local\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transform retrieved successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, unknown space name, or read failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/set": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Write transform values",
        "description": "Returns 200 with {ok: true, action: \u0022transform.set\u0022} on success.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found, \u0022space\u0022 is not one of its names, a supplied channel is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or the write is rejected, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nOmitted channels are left unchanged; pass only the channels that should change. See POST /api/actions/transform/get for round-trip reads and POST /api/actions/tween/position for an animated form.\r\n            \r\n*Example: When a channel-point reward redeems \u0022put the streamer on the moon\u0022, set the streamer avatar\u0027s world position to the moon-mark coordinates so the next camera cut lands on the gag.*",
        "operationId": "Actions_TransformSet",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3), \u0022rotation\u0022 (Vector3), \u0022scale\u0022 (Vector3), and \u0022space\u0022 (String, \u0022world\u0022 or \u0022local\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transform updated successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, unknown space name, malformed channel value, or write failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/getDirection": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Read transform direction axis",
        "description": "Returns 200 with the direction Vector3 (unit-length in world space) as the response body; the response is the bare [x, y, z] vector rather than a wrapped object.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022direction\u0022 is not one of its names, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/setDirection to drive the same axis from an external vector and POST /api/actions/transform/lookAt to aim at a point or scene object instead.\r\n            \r\n*Example: When a viewer-driven projectile launcher fires, read the launcher scene object\u0027s forward direction so the spawned shot inherits the muzzle aim instead of flying along a fixed axis.*",
        "operationId": "Actions_TransformGetDirection",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional key \u0022direction\u0022 (String, one of \u0022forward\u0022, \u0022back\u0022, \u0022left\u0022, \u0022right\u0022, \u0022up\u0022, or \u0022down\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Direction retrieved successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found or unknown direction name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/setDirection": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Aim transform along a vector",
        "description": "Returns 200 with {ok: true, action: \u0022transform.setDirection\u0022} on success; the input vector is normalized internally.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, \u0022direction\u0022 is not one of its names, \u0022input_direction\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or the input vector has zero length, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/lookAt for a higher-level point-or-scene-object target and POST /api/actions/transform/getDirection to read the resulting axis back.\r\n            \r\n*Example: When chat votes a new \u0022follow target\u0022 for a hovering drone prop, set the drone\u0027s forward direction to point at the chosen target so it visibly tracks the crowd\u0027s pick.*",
        "operationId": "Actions_TransformSetDirection",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022input_direction\u0022 (Vector3) and \u0022direction\u0022 (String, one of \u0022forward\u0022, \u0022back\u0022, \u0022left\u0022, \u0022right\u0022, \u0022up\u0022, or \u0022down\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Direction set successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, unknown direction name, or input vector invalid.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/lookAt": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Aim transform at a point",
        "description": "Returns 200 with {ok: true, action: \u0022transform.lookAt\u0022} on success; the rotation snaps immediately.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the source or named target scene object cannot be found, if \u0022look_at_object\u0022 is null, if neither \u0022look_at_object\u0022 nor \u0022position\u0022 is supplied (a null \u0022position\u0022 counts as not supplied), or if \u0022position\u0022 or \u0022world_up\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key (a supplied \u0022position\u0022 is checked even beside \u0022look_at_object\u0022), and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/setDirection for vector-driven aiming and POST /api/actions/tween/rotation for an animated turn.\r\n            \r\n*Example: When a \u0022look at me\u0022 viewer reward fires, point the streamer-avatar head scene object at the named studio-camera scene object so the next take has clean eye contact.*",
        "operationId": "Actions_TransformLookAt",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022look_at_object\u0022 (String, another scene object), \u0022position\u0022 (Vector3), and \u0022world_up\u0022 (Vector3) - provide exactly one of \u0022look_at_object\u0022 or \u0022position\u0022 as the aim point.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Look-at applied successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Source or target scene object not found, a null \u0022look_at_object\u0022, no aim point supplied, or malformed Vector3 parameter.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/rotateAround": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Orbit transform around pivot",
        "description": "Returns 200 with {ok: true, action: \u0022transform.rotateAround\u0022} on success; the rotation is applied instantly and modifies both position and orientation as the scene object orbits the pivot.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found, \u0022pivot\u0022 or \u0022axis\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or the axis has zero length, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/set for axis-aligned spin around the scene object\u0027s own origin instead of an external pivot.\r\n            \r\n*Example: When chat reaches a sub-goal, orbit the trophy prop scene object around the central podium pivot so the on-stream camera catches a celebratory spin without manual repositioning.*",
        "operationId": "Actions_TransformRotateAround",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022axis\u0022 (Vector3, non-zero), \u0022pivot\u0022 (Vector3), and \u0022angle\u0022 (Float, degrees).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotation applied successfully."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed pivot, or axis invalid.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/smoothMove": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Smooth-move toward a position",
        "description": "Returns 200 with the bare object {tag, name, active} as soon as the move begins, plus {tweenId, duration} when smoothTime is above 0. The move is a fixed-duration tween with the easeOutCubic curve: the runtime drives the position to the target over exactly smoothTime seconds, fast at first and slowing into the target, and lands on it when the duration ends. The response is not wrapped in an ok/action envelope.\r\nThe move runs on the scene object\u0027s position channel exactly as POST /api/actions/tween/position does, so it reports tween.completed when it arrives, and POST /api/actions/tween/cancel or a later \u0022replaceExisting\u0022 position tween stops it.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String, 422 ACTION_FAILED if the named scene object cannot be found or \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position for the same fixed-duration move with a choice of easing curve, POST /api/actions/tween/cancel to stop the move early, and POST /api/actions/transform/set for snap moves.\r\n            \r\n*Example: When the streamer triggers a \u0022follow me\u0022 hotkey, smooth-move the companion drone scene object toward the streamer-avatar position so the prop glides into frame without jitter.*",
        "operationId": "Actions_TransformSmoothMove",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3, world-space target), \u0022smoothTime\u0022 (Float, seconds), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Smooth move started successfully."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found or malformed position.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/viewport/toWorld": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Convert viewport to world",
        "description": "Returns 200 with the Vector3 result as the response body; (0, 0) is the bottom-left corner of the viewport and (1, 1) is the top-right.\r\nReturns 422 ACTION_FAILED if no active camera is available or the named camera cannot be resolved, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe equivalent graph node takes all three coordinates on one \u0022Viewport Pos\u0022 vector port whose Z component carries the depth, and that port defaults to 0.5 on every component, so a node left at its defaults and a request that omits \u0022depth\u0022 project to different distances.\r\nSee POST /api/actions/world/toViewport for the inverse conversion and POST /api/actions/transform/set to drop a scene object at the resulting point.\r\n            \r\n*Example: When a viewer drags an emoji onto the stream overlay, convert the overlay\u0027s normalized drop coordinates into world space so a prop can be spawned exactly under the cursor in the live scene.*",
        "operationId": "Actions_ViewportToWorld",
        "requestBody": {
          "description": "Optional keys \u0022x\u0022 (Float, 0-1), \u0022y\u0022 (Float, 0-1), \u0022depth\u0022 (Float, world units from the camera), and \u0022camera\u0022 (String, named camera scene object).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion completed."
          },
          "422": {
            "description": "No active camera available.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/world/toViewport": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Convert world to viewport",
        "description": "Returns 200 with the bare object {x, y, z} on success, where x and y are normalized viewport coordinates and z is the depth in world units from the camera. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if no active camera is available, the named camera cannot be resolved, or \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/viewport/toWorld for the inverse conversion.\r\n            \r\n*Example: When a chat reward attaches a \u0022name tag\u0022 overlay to a tracked scene object, project the scene object\u0027s world position into viewport coordinates so the overlay element snaps to the right pixel each frame.*",
        "operationId": "Actions_WorldToViewport",
        "requestBody": {
          "description": "Optional keys \u0022position\u0022 (Vector3) and \u0022camera\u0022 (String, named camera scene object).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion completed."
          },
          "422": {
            "description": "No active camera available or malformed position.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/viewport/place": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Place object at viewport point",
        "description": "Placement is world-space only: there is no \u0022space\u0022 parameter, position always comes from projecting \u0022x\u0022, \u0022y\u0022, and \u0022depth\u0022 through the resolved camera, and \u0022rotation\u0022 and \u0022scale\u0022 are written in world and local space respectively exactly as POST /api/actions/transform/set writes them. Only the channels actually supplied are written, so omitting \u0022rotation\u0022 and \u0022scale\u0022 moves the scene object and leaves its orientation and size alone. The whole sequence runs inside one frame, and \u0022position\u0022, \u0022rotation\u0022, and \u0022scale\u0022 on the response are the authoritative values read back after the write rather than the values that were requested. \u0022viewport\u0022 reprojects that written world position back through the same camera, so a caller can see where the scene object actually landed, with z carrying depth in world units from the camera. \u0022camera\u0022 reports the resolved camera tag, which is the tag that was sent when one was sent and is null in the one case where the active camera carries no tag of its own, and \u0022frame\u0022 is the runtime frame the write landed on, which serves as the revision for this placement.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, 404 OBJECT_NOT_FOUND when the \u0022object\u0022 tag does not resolve, 404 CAMERA_NOT_FOUND when a named camera does not resolve or carries no camera or when no active camera exists, 422 ACTION_FAILED when the placement fails for any other reason, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee POST /api/actions/viewport/toWorld to convert a viewport point without writing anything, POST /api/actions/world/toViewport for the reverse projection, and POST /api/actions/transform/set to write a world position directly.\r\n            \r\n*Example: When chat says \u0027!pin\u0027, place the mascot prop scene object at the top-right corner of frame so it holds that spot on screen no matter where the camera sits.*",
        "operationId": "Actions_ViewportPlace",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022x\u0022 (Float, 0-1), \u0022y\u0022 (Float, 0-1), \u0022depth\u0022 (Float, world units from the camera), \u0022camera\u0022 (String, named camera scene object), \u0022rotation\u0022 (Vector3, Euler degrees), \u0022scale\u0022 (Vector3), and \u0022requestId\u0022 (String, echoed back on the response).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene object placed."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object or named camera not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Placement failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/position": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Tween position over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime drives the position over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the position in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.position\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/smoothMove for a fixed-duration move with the easeOutCubic curve built in, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/tween/rotation or POST /api/actions/tween/scale for the matching channels.\r\n            \r\n*Example: When a subscriber milestone lands, tween the celebratory trophy prop scene object from off-camera into the center spotlight position over two seconds with an ease-out curve so the entrance reads as deliberate.*",
        "operationId": "Actions_TweenPosition",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3, world-space target), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Position tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed position, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/rotation": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Tween rotation over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime eases each Euler axis separately from the world Euler angles it samples on its first update pass, each axis taking the shorter way round, and applies the named easing curve over the requested duration. The route is therefore not a single Quaternion arc; keep each axis target within half a turn of its current value when the exact route matters. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the rotation in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.rotation\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022rotation\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/lookAt for snap aiming, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/tween/position or POST /api/actions/tween/scale for the matching channels.\r\n            \r\n*Example: When chat votes \u0022tilt the camera dramatic\u0022, tween the camera rig scene object to a 15-degree dutch angle over one second with an ease-in-out curve so the reveal feels cinematic instead of glitchy.*",
        "operationId": "Actions_TweenRotation",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022rotation\u0022 (Vector3), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotation tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed rotation, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/scale": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Tween scale over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime drives local scale over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the scale in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.scale\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022scale\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position or POST /api/actions/tween/rotation for the matching channels, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/transform/set for snap-scale writes.\r\n            \r\n*Example: When a donation total crosses a threshold, tween the on-stage banner prop scene object up to 1.5x scale and back over three seconds so chat sees a physical \u0022pop\u0022 celebrating the milestone.*",
        "operationId": "Actions_TweenScale",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022scale\u0022 (Vector3, local-space target), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scale tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed scale, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/transform": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Tween multiple transform channels together",
        "description": "Returns 200 with the bare object {tag, groupId, channels} as soon as the group starts; every named channel begins inside the same runtime update pass, so the channels leave their start values together rather than one frame apart. The response is not wrapped in an ok/action envelope.\r\nChannels whose resolved duration is 0 or less are written instantly, mint no identifier, and are left out of the channels array; when every named channel resolves that way, groupId is null and channels is empty. The object shape is always returned, never a bare null.\r\nStaggered finish and group completion: each channel can carry its own duration, so when the durations differ the channels of one group finish at different moments, and each reports its own tween.completed carrying the shared groupId. The group is finished when the last of its tweenIds has reported; there is no separate group-completion event, so a client that needs one counts the ids the start call returned.\r\nRotation start sampling: the rotation channel samples its start as world Euler angles on its first update pass, then eases each Euler axis separately toward the target, each axis taking the shorter way round, so its route is not a single Quaternion arc and can differ from the turn the target numbers suggest. Keep each axis target within half a turn of its current value when the exact route matters.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied at the shared level. Returns 422 ACTION_FAILED if the named scene object cannot be found, if none of \u0022position\u0022, \u0022rotation\u0022, or \u0022scale\u0022 is supplied, if a channel value is neither an array of at least three numbers, an {x, y, z} object, nor an object whose \u0022to\u0022 is one of those (entries after the third are ignored), if one channel object carries both \u0022time\u0022 and \u0022duration\u0022, or if an \u0022ease\u0022 name falls outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position, POST /api/actions/tween/rotation, and POST /api/actions/tween/scale for driving one channel at a time, POST /api/actions/tween/cancel with the returned \u0022groupId\u0022 to stop the whole group, and POST /api/actions/transform/set for snap writes with no animation.\r\n            \r\n*Example: When a raid lands, sweep the welcome banner scene object into frame, spin it a quarter turn, and swell it to 1.2x in one co-started move so the whole entrance reads as a single gesture instead of three separate animations.*",
        "operationId": "Actions_TweenTransform",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3 or channel object), \u0022rotation\u0022 (Vector3 or channel object), \u0022scale\u0022 (Vector3 or channel object), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transform group started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied at the shared level.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, no channel supplied, malformed channel value, conflicting per-channel \u0022time\u0022 and \u0022duration\u0022, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/path": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Tween along a curved path",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the path tween starts; the runtime drives the object along the whole path over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA path move is one tween on the position channel, so it carries a single tweenId and no groupId, and POST /api/actions/tween/cancel reaches it by that tweenId or by channel \u0022position\u0022. Both modes drive absolute positions, so the object snaps to the first waypoint at the start of the move; pass its current position as the first waypoint when the motion has to be continuous.\r\nA duration of 0 or less places the object on the final waypoint in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.path\u0022} rather than the object shape, exactly as the single-channel tween actions behave for an instant set.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, when \u0022points\u0022 is omitted, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, if \u0022points\u0022 is not an array (a String is refused even when it holds a JSON array of points), carries fewer than two entries, or holds an entry that is neither an array of exactly three numbers nor an {x, y, z} object, or if \u0022mode\u0022, \u0022space\u0022, or \u0022ease\u0022 carries an unlisted value, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position for a straight move to a single target, POST /api/actions/tween/transform for driving several channels at once, and POST /api/actions/tween/cancel to stop a path already in flight.\r\n            \r\n*Example: When a chat vote picks the studio tour, fly the camera rig scene object along a four-waypoint arc past the desk, the shelf, and back to the seat over eight seconds with orientToPath on, so the shot turns to face its direction of travel through every bend.*",
        "operationId": "Actions_TweenPath",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022points\u0022 (Array, the waypoints in visit order, each entry a Vector3). Optional keys \u0022mode\u0022 (String, \u0022bezier\u0022 or \u0022spline\u0022), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), \u0022space\u0022 (String, \u0022world\u0022 or \u0022local\u0022), \u0022orientToPath\u0022 (Boolean), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Path tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, \u0022points\u0022 omitted, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, \u0022points\u0022 not an array (a String included) or malformed, or unknown mode, space, or ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/transform/tween/cancel": {
      "post": {
        "tags": [
          "L2 Transform: Actions"
        ],
        "summary": "Stop running tweens",
        "description": "Returns 200 with the bare object {tag, cancelled} on success; each stopped tween holds the value it had reached rather than snapping to its target. Every tracked tween is in scope, whichever of the tween actions, transform.smoothMove, or a graph Position Tween, Rotation Tween, Scale Tween, or Path Tween node started it. The response is not wrapped in an ok/action envelope.\r\nScope precedence: tweenId (one tween) beats groupId (every channel of one co-started group), which beats channel (every tracked tween on that channel), and with none of the three supplied every tracked tween on the scene object stops. A group returned by POST /api/actions/tween/transform therefore cancels as a unit in one call, with its entries listed in ascending tweenId order.\r\nEvery stopped tween emits tween.cancelled with reason \u0022explicit\u0022 and never also emits tween.completed.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String. Returns 422 ACTION_FAILED if the named scene object cannot be found, if \u0022tweenId\u0022 names no tween still running on it, if \u0022groupId\u0022 names no group still running on it, or if \u0022channel\u0022 carries a value outside the four listed above, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position, POST /api/actions/tween/rotation, POST /api/actions/tween/scale, POST /api/actions/tween/transform, and POST /api/actions/tween/path for the tweens this stops, and their \u0022replaceExisting\u0022 parameter for handing one channel straight to a replacement.\r\n            \r\n*Example: When a viewer redeem interrupts a slow banner slide, stop the banner prop scene object\u0027s position tween so the overlay holds where it is instead of drifting through the next camera cut.*",
        "operationId": "Actions_TweenCancel",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022tweenId\u0022 (Integer), \u0022groupId\u0022 (Integer), and \u0022channel\u0022 (String, one of \u0022position\u0022, \u0022rotation\u0022, \u0022scale\u0022, or \u0022all\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tweens stopped."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, tweenId or groupId not running, or unknown channel.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/position": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Tween position over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime drives the position over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the position in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.position\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/smoothMove for a fixed-duration move with the easeOutCubic curve built in, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/tween/rotation or POST /api/actions/tween/scale for the matching channels.\r\n            \r\n*Example: When a subscriber milestone lands, tween the celebratory trophy prop scene object from off-camera into the center spotlight position over two seconds with an ease-out curve so the entrance reads as deliberate.*",
        "operationId": "Actions_TweenPosition",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3, world-space target), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Position tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed position, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/rotation": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Tween rotation over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime eases each Euler axis separately from the world Euler angles it samples on its first update pass, each axis taking the shorter way round, and applies the named easing curve over the requested duration. The route is therefore not a single Quaternion arc; keep each axis target within half a turn of its current value when the exact route matters. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the rotation in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.rotation\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022rotation\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/lookAt for snap aiming, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/tween/position or POST /api/actions/tween/scale for the matching channels.\r\n            \r\n*Example: When chat votes \u0022tilt the camera dramatic\u0022, tween the camera rig scene object to a 15-degree dutch angle over one second with an ease-in-out curve so the reveal feels cinematic instead of glitchy.*",
        "operationId": "Actions_TweenRotation",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022rotation\u0022 (Vector3), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotation tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed rotation, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/scale": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Tween scale over time",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the tween starts; the runtime drives local scale over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA duration of 0 or less writes the scale in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.scale\u0022} rather than the object shape.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, \u0022scale\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, or \u0022ease\u0022 carries a name outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position or POST /api/actions/tween/rotation for the matching channels, POST /api/actions/tween/cancel to stop a tween already in flight, and POST /api/actions/transform/set for snap-scale writes.\r\n            \r\n*Example: When a donation total crosses a threshold, tween the on-stage banner prop scene object up to 1.5x scale and back over three seconds so chat sees a physical \u0022pop\u0022 celebrating the milestone.*",
        "operationId": "Actions_TweenScale",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022scale\u0022 (Vector3, local-space target), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scale tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, malformed scale, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/transform": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Tween multiple transform channels together",
        "description": "Returns 200 with the bare object {tag, groupId, channels} as soon as the group starts; every named channel begins inside the same runtime update pass, so the channels leave their start values together rather than one frame apart. The response is not wrapped in an ok/action envelope.\r\nChannels whose resolved duration is 0 or less are written instantly, mint no identifier, and are left out of the channels array; when every named channel resolves that way, groupId is null and channels is empty. The object shape is always returned, never a bare null.\r\nStaggered finish and group completion: each channel can carry its own duration, so when the durations differ the channels of one group finish at different moments, and each reports its own tween.completed carrying the shared groupId. The group is finished when the last of its tweenIds has reported; there is no separate group-completion event, so a client that needs one counts the ids the start call returned.\r\nRotation start sampling: the rotation channel samples its start as world Euler angles on its first update pass, then eases each Euler axis separately toward the target, each axis taking the shorter way round, so its route is not a single Quaternion arc and can differ from the turn the target numbers suggest. Keep each axis target within half a turn of its current value when the exact route matters.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied at the shared level. Returns 422 ACTION_FAILED if the named scene object cannot be found, if none of \u0022position\u0022, \u0022rotation\u0022, or \u0022scale\u0022 is supplied, if a channel value is neither an array of at least three numbers, an {x, y, z} object, nor an object whose \u0022to\u0022 is one of those (entries after the third are ignored), if one channel object carries both \u0022time\u0022 and \u0022duration\u0022, or if an \u0022ease\u0022 name falls outside the curated curve list, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position, POST /api/actions/tween/rotation, and POST /api/actions/tween/scale for driving one channel at a time, POST /api/actions/tween/cancel with the returned \u0022groupId\u0022 to stop the whole group, and POST /api/actions/transform/set for snap writes with no animation.\r\n            \r\n*Example: When a raid lands, sweep the welcome banner scene object into frame, spin it a quarter turn, and swell it to 1.2x in one co-started move so the whole entrance reads as a single gesture instead of three separate animations.*",
        "operationId": "Actions_TweenTransform",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022position\u0022 (Vector3 or channel object), \u0022rotation\u0022 (Vector3 or channel object), \u0022scale\u0022 (Vector3 or channel object), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transform group started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, or both \u0022time\u0022 and \u0022duration\u0022 supplied at the shared level.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, no channel supplied, malformed channel value, conflicting per-channel \u0022time\u0022 and \u0022duration\u0022, or unknown ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/path": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Tween along a curved path",
        "description": "Returns 200 with the bare object {tweenId, tag, duration} as soon as the path tween starts; the runtime drives the object along the whole path over the requested duration applying the named easing curve. The response is not wrapped in an ok/action envelope.\r\nA path move is one tween on the position channel, so it carries a single tweenId and no groupId, and POST /api/actions/tween/cancel reaches it by that tweenId or by channel \u0022position\u0022. Both modes drive absolute positions, so the object snaps to the first waypoint at the start of the move; pass its current position as the first waypoint when the motion has to be continuous.\r\nA duration of 0 or less places the object on the final waypoint in that same frame, mints no tweenId, fires no completion event, and answers 200 with {ok: true, action: \u0022tween.path\u0022} rather than the object shape, exactly as the single-channel tween actions behave for an instant set.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, when \u0022points\u0022 is omitted, or when both \u0022time\u0022 and \u0022duration\u0022 are supplied. Returns 422 ACTION_FAILED if the named scene object cannot be found, if \u0022points\u0022 is not an array (a String is refused even when it holds a JSON array of points), carries fewer than two entries, or holds an entry that is neither an array of exactly three numbers nor an {x, y, z} object, or if \u0022mode\u0022, \u0022space\u0022, or \u0022ease\u0022 carries an unlisted value, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position for a straight move to a single target, POST /api/actions/tween/transform for driving several channels at once, and POST /api/actions/tween/cancel to stop a path already in flight.\r\n            \r\n*Example: When a chat vote picks the studio tour, fly the camera rig scene object along a four-waypoint arc past the desk, the shelf, and back to the seat over eight seconds with orientToPath on, so the shot turns to face its direction of travel through every bend.*",
        "operationId": "Actions_TweenPath",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String) and \u0022points\u0022 (Array, the waypoints in visit order, each entry a Vector3). Optional keys \u0022mode\u0022 (String, \u0022bezier\u0022 or \u0022spline\u0022), \u0022time\u0022 (Float, seconds), \u0022duration\u0022 (Float, alias of time), \u0022ease\u0022 (String, easing curve name), \u0022space\u0022 (String, \u0022world\u0022 or \u0022local\u0022), \u0022orientToPath\u0022 (Boolean), and \u0022replaceExisting\u0022 (Boolean).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Path tween started."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String, \u0022points\u0022 omitted, or both \u0022time\u0022 and \u0022duration\u0022 supplied.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, \u0022points\u0022 not an array (a String included) or malformed, or unknown mode, space, or ease name.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/tween/cancel": {
      "post": {
        "tags": [
          "L2 Tween: Actions"
        ],
        "summary": "Stop running tweens",
        "description": "Returns 200 with the bare object {tag, cancelled} on success; each stopped tween holds the value it had reached rather than snapping to its target. Every tracked tween is in scope, whichever of the tween actions, transform.smoothMove, or a graph Position Tween, Rotation Tween, Scale Tween, or Path Tween node started it. The response is not wrapped in an ok/action envelope.\r\nScope precedence: tweenId (one tween) beats groupId (every channel of one co-started group), which beats channel (every tracked tween on that channel), and with none of the three supplied every tracked tween on the scene object stops. A group returned by POST /api/actions/tween/transform therefore cancels as a unit in one call, with its entries listed in ascending tweenId order.\r\nEvery stopped tween emits tween.cancelled with reason \u0022explicit\u0022 and never also emits tween.completed.\r\nReturns 400 INVALID_REQUEST if \u0022object\u0022 is missing or not a String. Returns 422 ACTION_FAILED if the named scene object cannot be found, if \u0022tweenId\u0022 names no tween still running on it, if \u0022groupId\u0022 names no group still running on it, or if \u0022channel\u0022 carries a value outside the four listed above, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position, POST /api/actions/tween/rotation, POST /api/actions/tween/scale, POST /api/actions/tween/transform, and POST /api/actions/tween/path for the tweens this stops, and their \u0022replaceExisting\u0022 parameter for handing one channel straight to a replacement.\r\n            \r\n*Example: When a viewer redeem interrupts a slow banner slide, stop the banner prop scene object\u0027s position tween so the overlay holds where it is instead of drifting through the next camera cut.*",
        "operationId": "Actions_TweenCancel",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022tweenId\u0022 (Integer), \u0022groupId\u0022 (Integer), and \u0022channel\u0022 (String, one of \u0022position\u0022, \u0022rotation\u0022, \u0022scale\u0022, or \u0022all\u0022).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tweens stopped."
          },
          "400": {
            "description": "\u0022object\u0022 missing or not a String.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Scene object not found, tweenId or groupId not running, or unknown channel.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/variables/create": {
      "post": {
        "tags": [
          "L2 Variables: Actions"
        ],
        "summary": "Create a runtime variable",
        "description": "Returns 200 with the bare identifier string of the new variable as the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST when \u0022type\u0022 or \u0022name\u0022 is missing or \u0022name\u0022 is not a String, and 400 INVALID_TYPE when \u0022type\u0022 is not one of the five canonical labels. Returns 422 ACTION_FAILED if a variable with the same name already exists, an Integer, Float, or Boolean \u0022value\u0022 cannot be parsed, or a Vector3 \u0022value\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee GET /api/variables/valid-types for the canonical list and POST /api/actions/variables/set to update an existing variable.\r\n            \r\n*Example: When a viewer redeems a \u0022name on screen\u0022 channel point reward, create a String variable seeded with their display name so a Text overlay binds to it for the next stinger.*",
        "operationId": "Actions_VariablesCreate",
        "requestBody": {
          "description": "Required dictionary with keys \u0022name\u0022 (String) and \u0022type\u0022 (String, one of \u0022Boolean\u0022, \u0022Float\u0022, \u0022Integer\u0022, \u0022String\u0022, \u0022Vector3\u0022, case-insensitive). Optional key \u0022value\u0022 (any) for the initial value.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variable created."
          },
          "400": {
            "description": "Missing \u0022name\u0022 or \u0022type\u0022, a non-String \u0022name\u0022, or a \u0022type\u0022 outside the five canonical labels.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., variable already exists.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/variables/delete": {
      "post": {
        "tags": [
          "L2 Variables: Actions"
        ],
        "summary": "Delete a runtime variable",
        "description": "Returns 200 with {ok: true, action: \u0022variables.delete\u0022} on success.\r\nReturns 422 ACTION_FAILED if no variable exists with the given name, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. Nodes that still reference the deleted variable will surface a runtime error on their next execution.\r\nSee POST /api/actions/variables/create to register a replacement and GET /api/variables to audit live variables.\r\n            \r\n*Example: When a \u0022donation goal of the week\u0022 stream segment ends, delete the goal Integer variable so the overlay and counter nodes detach cleanly before the next segment is set up.*",
        "operationId": "Actions_VariablesDelete",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variable deleted."
          },
          "422": {
            "description": "Action failed - e.g., variable not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/variables/get": {
      "post": {
        "tags": [
          "L2 Variables: Actions"
        ],
        "summary": "Read a runtime variable",
        "description": "Returns 200 with the bare variable value as the response body, with no value key. The response is not wrapped in an ok/action envelope.\r\nA Boolean, Float, Integer, or String variable returns its scalar, a Vector3 variable a bare [x, y, z] array, and an array variable only its metadata {type: \u0022array\u0022, variableType, sizeX, sizeY, size}; read its entries with POST /api/actions/arrays/get.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if no variable exists with the given name, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/variables/set to mutate the value and GET /api/variables/{id} for the registry-side view.\r\n            \r\n*Example: When a viewer types \u0022!subgoal\u0022 in chat, read the current \u0022donation_total\u0022 Integer variable and feed it into a Text Formatter node that shows \u0022we are at $X\u0022 on an on-stream caption.*",
        "operationId": "Actions_VariablesGet",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variable value retrieved."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., variable not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/variables/set": {
      "post": {
        "tags": [
          "L2 Variables: Actions"
        ],
        "summary": "Write a runtime variable",
        "description": "Returns 200 with {ok: true, action: \u0022variables.set\u0022} on success and emits a variable.changed event for live subscribers.\r\nReturns 400 INVALID_REQUEST if \u0022name\u0022 is missing or not a String, 422 ACTION_FAILED if no variable exists with the given name or the supplied value cannot be coerced to the declared type, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/variables/get to read the value back and POST /api/actions/variables/create to register a new variable instead.\r\n            \r\n*Example: When a \u0022shoutout\u0022 chat command runs, write the target streamer\u0027s display name into a String variable so an on-stream banner overlay rebinds to the new text.*",
        "operationId": "Actions_VariablesSet",
        "requestBody": {
          "description": "Required dictionary with key \u0022name\u0022 (String). Optional key \u0022value\u0022 (any, shaped to the variable\u0027s declared canonical type).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variable value set."
          },
          "400": {
            "description": "Missing or non-String \u0022name\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., type mismatch.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/viewport/toWorld": {
      "post": {
        "tags": [
          "L2 Viewport: Actions"
        ],
        "summary": "Convert viewport to world",
        "description": "Returns 200 with the Vector3 result as the response body; (0, 0) is the bottom-left corner of the viewport and (1, 1) is the top-right.\r\nReturns 422 ACTION_FAILED if no active camera is available or the named camera cannot be resolved, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe equivalent graph node takes all three coordinates on one \u0022Viewport Pos\u0022 vector port whose Z component carries the depth, and that port defaults to 0.5 on every component, so a node left at its defaults and a request that omits \u0022depth\u0022 project to different distances.\r\nSee POST /api/actions/world/toViewport for the inverse conversion and POST /api/actions/transform/set to drop a scene object at the resulting point.\r\n            \r\n*Example: When a viewer drags an emoji onto the stream overlay, convert the overlay\u0027s normalized drop coordinates into world space so a prop can be spawned exactly under the cursor in the live scene.*",
        "operationId": "Actions_ViewportToWorld",
        "requestBody": {
          "description": "Optional keys \u0022x\u0022 (Float, 0-1), \u0022y\u0022 (Float, 0-1), \u0022depth\u0022 (Float, world units from the camera), and \u0022camera\u0022 (String, named camera scene object).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion completed."
          },
          "422": {
            "description": "No active camera available.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/viewport/place": {
      "post": {
        "tags": [
          "L2 Viewport: Actions"
        ],
        "summary": "Place object at viewport point",
        "description": "Placement is world-space only: there is no \u0022space\u0022 parameter, position always comes from projecting \u0022x\u0022, \u0022y\u0022, and \u0022depth\u0022 through the resolved camera, and \u0022rotation\u0022 and \u0022scale\u0022 are written in world and local space respectively exactly as POST /api/actions/transform/set writes them. Only the channels actually supplied are written, so omitting \u0022rotation\u0022 and \u0022scale\u0022 moves the scene object and leaves its orientation and size alone. The whole sequence runs inside one frame, and \u0022position\u0022, \u0022rotation\u0022, and \u0022scale\u0022 on the response are the authoritative values read back after the write rather than the values that were requested. \u0022viewport\u0022 reprojects that written world position back through the same camera, so a caller can see where the scene object actually landed, with z carrying depth in world units from the camera. \u0022camera\u0022 reports the resolved camera tag, which is the tag that was sent when one was sent and is null in the one case where the active camera carries no tag of its own, and \u0022frame\u0022 is the runtime frame the write landed on, which serves as the revision for this placement.\r\nReturns 400 INVALID_REQUEST when \u0022object\u0022 is missing or not a String, 404 OBJECT_NOT_FOUND when the \u0022object\u0022 tag does not resolve, 404 CAMERA_NOT_FOUND when a named camera does not resolve or carries no camera or when no active camera exists, 422 ACTION_FAILED when the placement fails for any other reason, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee POST /api/actions/viewport/toWorld to convert a viewport point without writing anything, POST /api/actions/world/toViewport for the reverse projection, and POST /api/actions/transform/set to write a world position directly.\r\n            \r\n*Example: When chat says \u0027!pin\u0027, place the mascot prop scene object at the top-right corner of frame so it holds that spot on screen no matter where the camera sits.*",
        "operationId": "Actions_ViewportPlace",
        "requestBody": {
          "description": "Required dictionary with key \u0022object\u0022 (String). Optional keys \u0022x\u0022 (Float, 0-1), \u0022y\u0022 (Float, 0-1), \u0022depth\u0022 (Float, world units from the camera), \u0022camera\u0022 (String, named camera scene object), \u0022rotation\u0022 (Vector3, Euler degrees), \u0022scale\u0022 (Vector3), and \u0022requestId\u0022 (String, echoed back on the response).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scene object placed."
          },
          "400": {
            "description": "Missing or non-String \u0022object\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Scene object or named camera not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Placement failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/web/http-request": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send an HTTP request",
        "description": "Returns 200 with the bare object {body, statusCode, headers, bytes} on success, where bytes carries the base64 form of the response body. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022url\u0022, \u0022uri\u0022, \u0022method\u0022, or \u0022body\u0022 is not a String or \u0022timeout\u0022 is not a number. Returns 422 ACTION_FAILED if neither \u0022url\u0022 nor \u0022uri\u0022 is supplied or the one read is empty (\u0022url\u0022 is read first and wins when both are present), if \u0022headers\u0022 is not an array (a String or an object mapping names to values, refused before anything is sent), if \u0022timeout\u0022 is above 2147483647 (refused naming \u0022timeout\u0022 and its range, before anything is sent), if the URL is not a valid absolute http or https URL, if \u0022method\u0022 is empty or not a well-formed HTTP method name (one holding a space, for example), and when the request cannot be completed: a refused connection, a host that was not found, a secure connection that could not be established, another network error, or a timeout comfortably below 30000 milliseconds, the 25000 default included. The refusal names which of these happened. Returns 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected, and 504 OVERMOX_TIMEOUT when OverMox has not answered within 30 seconds, which a remote that does not answer causes with a timeout from close to 30000 up to 2147483647. A non-2xx reply is not an error: it returns 200 with its statusCode.\r\nDefault method is GET when \u0022method\u0022 is omitted. See POST /api/actions/web/webhook-send for a plain JSON POST that waits for the reply and returns only its body as a string, without the status code or headers.\r\n            \r\n*Example: When a viewer redeems a \u0022shoutout the channel\u0022 reward, GET the target channel\u0027s public profile and feed the avatar URL into an on-stream raid card.*",
        "operationId": "Actions_HttpRequest",
        "requestBody": {
          "description": "Optional keys \u0022url\u0022 (String), \u0022uri\u0022 (String, alternate spelling of url), \u0022method\u0022 (String), \u0022headers\u0022 (Array, \u0022Key: Value\u0022 String entries), \u0022body\u0022 (String), and \u0022timeout\u0022 (Float, milliseconds). Supply at least one of \u0022url\u0022 or \u0022uri\u0022; a call with neither returns 422 ACTION_FAILED.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "HTTP request completed."
          },
          "400": {
            "description": "\u0022url\u0022, \u0022uri\u0022, \u0022method\u0022, or \u0022body\u0022 not a String, or \u0022timeout\u0022 not a number.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed, e.g., a \u0022headers\u0022 value that is not an array, a \u0022timeout\u0022 above 2147483647, a refused connection, a host that was not found, or a timeout comfortably below 30000 milliseconds, the 25000 default included.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not answer within 30 seconds, e.g., a timeout from close to 30000 up to 2147483647 on a remote that does not answer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/web/remote-trigger": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send a remote trigger",
        "description": "Returns 200 with the bare object {id, receiversTriggered} on success, where receiversTriggered is the count of running Remote Receiver nodes whose id matched, and is 0 when none matched. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022id\u0022 is missing, 422 ACTION_FAILED if \u0022id\u0022 is an empty string, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. An id that matches no receiver is not an error and returns 200 with receiversTriggered 0.\r\nThe \u0022id\u0022 is matched against the id port of every Remote Receiver node in a running graph, and each match fires with \u0022message\u0022 copied onto its message port. See POST /api/actions/web/webhook-send for delivering to external HTTP endpoints instead.\r\n            \r\n*Example: When a chat command redeems a \u0022switch overlay\u0022 reward, fire the id that the overlay graph\u0027s Remote Receiver listens on so the graph runs its scene-change branch, and read receiversTriggered to confirm the graph was actually running.*",
        "operationId": "Actions_RemoteTrigger",
        "requestBody": {
          "description": "Required dictionary with key \u0022id\u0022 (String, trigger ID). Optional key \u0022message\u0022 (String) carrying a payload for the receiver.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remote trigger sent."
          },
          "400": {
            "description": "Missing required parameter \u0022id\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., empty \u0022id\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/web/webhook-send": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send a webhook",
        "description": "Returns 200 with the remote endpoint\u0027s reply body as a bare JSON string value when the endpoint answers a success (2xx) status. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022uri\u0022 is missing or \u0022uri\u0022 or \u0022payload\u0022 is not a String, 422 ACTION_FAILED if \u0022uri\u0022 is an empty string or not an absolute http or https URL, if the request cannot be completed (a refused connection, a host that was not found, a connection that timed out, a secure connection that could not be established, or another network error), if the remote endpoint has not answered within 25 seconds, or if it answers a non-success status, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. A request that cannot be completed is refused naming which of those failures happened, and a non-success status is refused naming the status code and its reason phrase; neither refusal echoes \u0022uri\u0022 or the endpoint\u0027s reply body, since a webhook URI often carries its secret. The send gives up on an endpoint that has not answered after 25 seconds, inside the 30 seconds the API waits for OverMox to answer, so a silent endpoint gets the 422 answer naming that wait rather than 504 OVERMOX_TIMEOUT; such an endpoint may still have received the payload. The wait is fixed; see POST /api/actions/web/http-request to choose one.\r\nThe payload is sent as raw text. See POST /api/actions/web/http-request when the reply status code, headers, or a non-success reply\u0027s body matter for downstream nodes.\r\n            \r\n*Example: When a donation event lands, POST a stringified JSON payload whose content field names the donor and amount to a Discord webhook so the streamer\u0027s mod channel mirrors on-stream alerts.*",
        "operationId": "Actions_WebhookSend",
        "requestBody": {
          "description": "Required dictionary with key \u0022uri\u0022 (String); optional key \u0022payload\u0022 (String). Both keys are honored by the runtime.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook sent and the endpoint answered a success status."
          },
          "400": {
            "description": "Missing or non-String \u0022uri\u0022, or a non-String \u0022payload\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed, e.g., an empty \u0022uri\u0022, a refused connection or a host that was not found, no answer within 25 seconds, or the endpoint answered a non-success status.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "OverMox did not respond in time.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/webhook/listen": {
      "post": {
        "tags": [
          "L2 Webhook: Actions"
        ],
        "summary": "Register a webhook listener",
        "description": "Returns 200 with the bare object {listenerId, url, path, createdAt} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 400 INVALID_REQUEST if \u0022path\u0022 is missing or not a String, 422 ACTION_FAILED if \u0022path\u0022 is an empty string, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. No path is refused as malformed or duplicate: every call mints a new listener with its own listenerId and url, even when the path repeats an earlier one.\r\nExternal services POST to the returned url, and each callback arrives via the L3 event webhook.received whose data carries listenerId, path, payload (the raw request body as a String), and timestamp. See POST /api/actions/webhook/unlisten to remove the listener and POST /api/actions/webhook/listeners for the registry.\r\n            \r\n*Example: When a streamer wires up a third-party donation service, register a \u0022/donations\u0022 listener and give the service the returned url, so each alert it POSTs arrives as a webhook.received event that fires confetti on screen.*",
        "operationId": "Actions_WebhookListen",
        "requestBody": {
          "description": "Required dictionary with key \u0022path\u0022 (String, a label for the listener; a leading \u0022/\u0022 is added when absent).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook listener registered."
          },
          "400": {
            "description": "Missing or non-String \u0022path\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Action failed - e.g., empty \u0022path\u0022.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/webhook/unlisten": {
      "post": {
        "tags": [
          "L2 Webhook: Actions"
        ],
        "summary": "Remove a webhook listener",
        "description": "Returns 200 with the bare object {removed} on success. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the listenerId is unknown or already removed, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nLook up active listenerId values via POST /api/actions/webhook/listeners. See POST /api/actions/webhook/listen for the registration counterpart.\r\n            \r\n*Example: When a streamer ends a charity fundraiser, unregister the donation webhook so third-party services stop firing alerts into a graph that is no longer in use.*",
        "operationId": "Actions_WebhookUnlisten",
        "requestBody": {
          "description": "Required dictionary with key \u0022listenerId\u0022 (String) returned by the original listen call.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook listener removed."
          },
          "422": {
            "description": "Action failed - e.g., listenerId not found.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/webhook/listeners": {
      "post": {
        "tags": [
          "L2 Webhook: Actions"
        ],
        "summary": "List webhook listeners",
        "description": "Returns 200 with the bare object {listeners} on success, where each entry carries {listenerId, url, path, createdAt}. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if the registry cannot be read, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nUse the listenerId values with POST /api/actions/webhook/unlisten. See POST /api/actions/webhook/listen for adding new listeners.\r\n            \r\n*Example: When a streamer audits their integrations before going live, list the active webhooks to confirm the donation, alert, and chat-relay endpoints are still wired.*",
        "operationId": "Actions_WebhookListeners",
        "requestBody": {
          "description": "Empty dictionary; no fields are read.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listeners returned."
          },
          "422": {
            "description": "Action failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/world/toViewport": {
      "post": {
        "tags": [
          "L2 World: Actions"
        ],
        "summary": "Convert world to viewport",
        "description": "Returns 200 with the bare object {x, y, z} on success, where x and y are normalized viewport coordinates and z is the depth in world units from the camera. The response is not wrapped in an ok/action envelope.\r\nReturns 422 ACTION_FAILED if no active camera is available, the named camera cannot be resolved, or \u0022position\u0022 is neither an [x, y, z] array nor an {x, y, z} object with a number for each key, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/viewport/toWorld for the inverse conversion.\r\n            \r\n*Example: When a chat reward attaches a \u0022name tag\u0022 overlay to a tracked scene object, project the scene object\u0027s world position into viewport coordinates so the overlay element snaps to the right pixel each frame.*",
        "operationId": "Actions_WorldToViewport",
        "requestBody": {
          "description": "Optional keys \u0022position\u0022 (Vector3) and \u0022camera\u0022 (String, named camera scene object).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion completed."
          },
          "422": {
            "description": "No active camera available or malformed position.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "OverMox is not connected.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/events/types": {
      "get": {
        "tags": [
          "L3: Events"
        ],
        "summary": "Get Layer 3 event catalog",
        "description": "Returns the full AsyncAPI 3.0.0 document listing every event the OverMox SignalR hub broadcasts at /api/events: the channel, the receive operation, and one message per event whose payload is the envelope (type, timestamp, data) with that event\u0027s data schema.\r\n\u0060python -m tools.sync fix\u0060 regenerates the document from the sync registry, and a Controller build copies it to the build output; if the Controller build output holds no asyncapi.json, the endpoint returns 503 ASYNCAPI_NOT_GENERATED with a recovery hint.\r\nSee GET /asyncapi for the rendered viewer and GET /llms.txt for the long-form catalog of streamer-facing capabilities.\r\n            \r\n*Example: When building a stream overlay that subscribes to chat-triggered scene events, fetch this catalog at startup to read each event\u0027s data schema before connecting to /api/events.*",
        "operationId": "Events_Types",
        "responses": {
          "200": {
            "description": "Returns the AsyncAPI 3.0 catalog as JSON.",
            "content": {
              "text/plain": {
                "schema": {}
              },
              "application/json": {
                "schema": {}
              },
              "text/json": {
                "schema": {}
              }
            }
          },
          "503": {
            "description": "asyncapi.json is missing from the Controller build output. Run \u0060python -m tools.sync fix\u0060 from the repo root, then rebuild and restart the OverMox Controller so the build copies Api/Data/asyncapi.json to its output. GET /api/events/types serves the catalog once the file is there.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ActionParameter": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Parameter name as it should appear in the JSON request body.",
            "nullable": true,
            "example": "asset"
          },
          "type": {
            "type": "string",
            "description": "Data type of the parameter. Common types: string, int, float, bool, float[3], float[4], object.",
            "nullable": true,
            "example": "string"
          },
          "required": {
            "type": "boolean",
            "description": "Whether this parameter is required. If false, the action will use a default value.",
            "example": true
          },
          "default": {
            "description": "Default value used when the parameter is not provided. Null if the parameter is required.",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Human-readable description of what this parameter controls.",
            "nullable": true,
            "example": "Bundled Asset name or primitive type (Cube, Sphere, Capsule, Cylinder, Plane, Quad)"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Valid values for this parameter, if constrained to a fixed set (e.g., easing functions).\r\nNull when any value of the declared type is accepted.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes a single parameter accepted by a Layer 2 action endpoint."
      },
      "ActionParameterSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "The dot-notated action string identifier (e.g., \u0022scene.spawn\u0022, \u0022transform.set\u0022).",
            "nullable": true,
            "example": "scene.spawn"
          },
          "description": {
            "type": "string",
            "description": "Human-readable description of what this action does.",
            "nullable": true,
            "example": "Spawns a new scene object from a Bundled Asset or primitive type"
          },
          "parameters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActionParameter"
            },
            "description": "Typed parameter definitions. Each parameter includes name, type, required flag, default value, and description.",
            "nullable": true
          },
          "returns": {
            "$ref": "#/components/schemas/ActionReturn"
          },
          "example": {
            "type": "object",
            "additionalProperties": {
              "nullable": true
            },
            "description": "A complete example request body showing typical parameter usage.",
            "nullable": true
          },
          "relatedNode": {
            "type": "string",
            "description": "The name of the node graph node that performs the same function, if any.\r\nUse this to cross-reference with \u0060GET /api/node-types/{category}/{node}\u0060.",
            "nullable": true,
            "example": "Instantiate"
          },
          "relatedCategory": {
            "type": "string",
            "description": "The category of the related node, if any.",
            "nullable": true,
            "example": "scene"
          },
          "endpoint": {
            "type": "string",
            "description": "The full API endpoint path for this action (e.g., \u0022/api/actions/transform/tween/position\u0022),\r\nor null for a handler-only action that dispatches internally with no REST route (the\r\nmath.* family, for example).",
            "nullable": true,
            "example": "/api/actions/scene/spawn"
          },
          "method": {
            "type": "string",
            "description": "The HTTP method to use when calling OverMoxController.Api.DTOs.Actions.ActionParameterSchema.Endpoint (e.g., \u0022POST\u0022 for an\r\naction-bus call, \u0022GET\u0022 for a read such as an animation playback query), or null for a\r\nhandler-only action that has no REST route.",
            "nullable": true,
            "example": "POST"
          }
        },
        "additionalProperties": false,
        "description": "Describes the full parameter schema for a Layer 2 direct action endpoint.\r\nAgents use this to know exactly what parameters to send when calling an action."
      },
      "ActionReturn": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Data type of the return value.",
            "nullable": true,
            "example": "string"
          },
          "description": {
            "type": "string",
            "description": "Description of the return value.",
            "nullable": true,
            "example": "The name/identifier of the spawned object"
          }
        },
        "additionalProperties": false,
        "description": "Describes what an action returns on success."
      },
      "ActionSchemaCatalogResponse": {
        "type": "object",
        "properties": {
          "totalCount": {
            "type": "integer",
            "description": "Total number of registered action schemas.",
            "format": "int32",
            "example": 24
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "List of all action categories.",
            "nullable": true,
            "example": [
              "scene",
              "transform",
              "physics",
              "audio"
            ]
          },
          "actions": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/ActionParameterSchema"
            },
            "description": "All action schemas keyed by their dot-notated action string.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full catalog of all Layer 2 action parameter schemas, keyed by action string."
      },
      "AddNodeRequest": {
        "required": [
          "category",
          "locationX",
          "locationY"
        ],
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "The node type category to instantiate (required). Must match a registered node type from the discovery endpoint.",
            "nullable": true,
            "example": "scene"
          },
          "name": {
            "type": "string",
            "description": "Optional display name for the node. If omitted, defaults to the category name.",
            "nullable": true,
            "example": "MyCustomName"
          },
          "locationX": {
            "type": "number",
            "description": "Horizontal position to place the node on the graph canvas (required).",
            "format": "double",
            "example": 350.0
          },
          "locationY": {
            "type": "number",
            "description": "Vertical position to place the node on the graph canvas (required).",
            "format": "double",
            "example": 200.0
          }
        },
        "additionalProperties": false,
        "description": "Request to add a new node to a graph."
      },
      "AnimationClipSummary": {
        "required": [
          "frameRate",
          "name",
          "wrapMode"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The clip name, matched against the source file\u0027s animation names.",
            "nullable": true,
            "example": "Idle"
          },
          "duration": {
            "type": "number",
            "description": "The clip length in seconds, accumulated from the source file\u0027s channel inputs. Null when\r\nthe source cannot be read.",
            "format": "double",
            "nullable": true
          },
          "frameRate": {
            "type": "number",
            "description": "The clip sample rate, read from the live imported clip.",
            "format": "double"
          },
          "wrapMode": {
            "type": "string",
            "description": "The clip wrap mode: loop, once, pingPong, clampForever, or default, read from the live\r\nimported clip.",
            "nullable": true
          },
          "rootMotion": {
            "type": "boolean",
            "description": "Classification only: true when any channel targets a scene-root node with a translation\r\nor rotation path. Null when the source cannot be read.",
            "nullable": true
          },
          "channelCount": {
            "type": "integer",
            "description": "The total number of animation channels in the clip. Null when the source cannot be read.",
            "format": "int32",
            "nullable": true
          },
          "morphCount": {
            "type": "integer",
            "description": "The number of channels driving morph target weights. Null when the source cannot be read.",
            "format": "int32",
            "nullable": true
          },
          "boneCount": {
            "type": "integer",
            "description": "The number of distinct nodes driven by non-pointer channels. Null when the source cannot\r\nbe read.",
            "format": "int32",
            "nullable": true
          },
          "constant": {
            "type": "boolean",
            "description": "True when every keyframe across the clip equals the first. Null unless a fresh import\r\ncapture backs the answer.",
            "nullable": true
          },
          "matchesRestPose": {
            "type": "boolean",
            "description": "True when every keyframe equals the model\u0027s rest pose. Null unless a fresh import capture\r\nbacks the answer.",
            "nullable": true
          },
          "bindings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClipBinding"
            },
            "description": "One entry per animation channel, in channel order, carrying the same shapes the\r\nsingle-clip introspection read returns. Present only when detail is \u0022bindings\u0022.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one animation clip: its name, timing, wrap mode, and channel makeup."
      },
      "AnimationClipsResponse": {
        "required": [
          "action",
          "appliedAt",
          "clips",
          "frame",
          "object",
          "ok"
        ],
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the read dispatched and returned a clip list."
          },
          "action": {
            "type": "string",
            "description": "The action string that produced this response (animation.listClips).",
            "nullable": true,
            "example": "animation.listClips"
          },
          "object": {
            "type": "string",
            "description": "The resolved scene object tag whose clips were listed.",
            "nullable": true
          },
          "frame": {
            "type": "integer",
            "description": "The runtime frame on which the listing was captured.",
            "format": "int64"
          },
          "appliedAt": {
            "type": "number",
            "description": "The runtime time, in seconds, at which the listing was captured.",
            "format": "double"
          },
          "source": {
            "type": "string",
            "description": "The lowercase source file extension without a dot (for example glb or gltf). Null on an\r\nunavailable read, except the unsupported-format case, where it carries the rejected\r\nextension.",
            "nullable": true
          },
          "introspection": {
            "type": "string",
            "description": "Set to \u0022unavailable\u0022 when the object\u0027s model has no readable source. Absent on an\r\nintrospectable read.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "A human-readable explanation of why introspection was unavailable. Absent on an\r\nintrospectable read.",
            "nullable": true
          },
          "clips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnimationClipSummary"
            },
            "description": "One row per clip on the object\u0027s model, in model order; empty when the model carries none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries every animation clip on a scene object as one typed row per clip, with the read\u0027s\r\ndispatch metadata."
      },
      "AnimationPlaybackSnapshot": {
        "type": "object",
        "properties": {
          "playbackId": {
            "type": "string",
            "description": "The playback session id, or null for a playback started outside a tracked request.",
            "nullable": true
          },
          "clip": {
            "type": "string",
            "description": "The clip name this playback drives.",
            "nullable": true
          },
          "state": {
            "type": "string",
            "description": "The playback state: playing, paused, held (a clampForever end hold), or fading\r\n(an active weight ramp).",
            "nullable": true
          },
          "paused": {
            "type": "boolean",
            "description": "True while the playback is paused on its current frame."
          },
          "time": {
            "type": "number",
            "description": "The clip-local playback position in seconds.",
            "format": "float"
          },
          "normalizedTime": {
            "type": "number",
            "description": "The raw engine position: negative under reverse loop, unbounded under pingPong.",
            "format": "float"
          },
          "phase": {
            "type": "number",
            "description": "The reflected clip-local position in the range 0 to 1.",
            "format": "float"
          },
          "cycle": {
            "type": "integer",
            "description": "floor(normalizedTime): counts legs under pingPong and goes negative under reverse loop.",
            "format": "int32"
          },
          "direction": {
            "type": "string",
            "description": "The current travel direction: forward or backward.",
            "nullable": true
          },
          "duration": {
            "type": "number",
            "description": "The clip length in seconds.",
            "format": "float"
          },
          "speed": {
            "type": "number",
            "description": "The playback speed multiplier; negative values play in reverse.",
            "format": "float"
          },
          "weight": {
            "type": "number",
            "description": "The current blend weight applied to the playback.",
            "format": "float"
          },
          "blendMode": {
            "type": "string",
            "description": "How the playback composites: additive or blend.",
            "nullable": true
          },
          "requestedLoopMode": {
            "type": "string",
            "description": "The loop mode the play request asked for: once, loop, pingPong, clampForever, or default.",
            "nullable": true
          },
          "effectiveLoopMode": {
            "type": "string",
            "description": "The wrap mode actually in force for the clip.",
            "nullable": true
          },
          "layer": {
            "type": "integer",
            "description": "The blend layer this playback holds: the layer the play request named, or the smallest\r\nfree index the runtime picked when the request omitted one (serial plays report 0).\r\nSet a layer at play time with the layer parameter on animation.play or animation.playSet.",
            "format": "int32"
          },
          "group": {
            "type": "string",
            "description": "The group name carried by the play request, or null when the play request carried no group.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one active or held animation playback: its clip, timing, loop mode, and\r\nblend state."
      },
      "AnimationPlaybacksResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the read dispatched and returned a snapshot list."
          },
          "action": {
            "type": "string",
            "description": "The action string that produced this response (animation.getPlaybacks).",
            "nullable": true,
            "example": "animation.getPlaybacks"
          },
          "object": {
            "type": "string",
            "description": "The resolved scene object tag whose playbacks were read.",
            "nullable": true
          },
          "frame": {
            "type": "integer",
            "description": "The runtime frame on which the snapshot list was captured.",
            "format": "int64"
          },
          "appliedAt": {
            "type": "number",
            "description": "The runtime time, in seconds, at which the snapshot list was captured.",
            "format": "double"
          },
          "playbacks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnimationPlaybackSnapshot"
            },
            "description": "One entry per active or held playback on the object; empty when nothing plays.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries the live playback snapshots read from a scene object, with the read\u0027s\r\ndispatch metadata."
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code.",
            "nullable": true,
            "example": "NOT_FOUND"
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message.",
            "nullable": true,
            "example": "Graph with the specified ID was not found."
          },
          "details": {
            "description": "Optional additional details about the error (validation failures, stack traces, etc.).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes an error returned by the API."
      },
      "ApiErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiError"
          }
        },
        "additionalProperties": false,
        "description": "Wrapper for API error responses returned on non-success status codes."
      },
      "ApiRootLinks": {
        "type": "object",
        "properties": {
          "health": {
            "type": "string",
            "description": "Health check endpoint.",
            "nullable": true,
            "example": "/api/health"
          },
          "llmsTxt": {
            "type": "string",
            "description": "LLM-friendly plain text guide.",
            "nullable": true,
            "example": "/llms.txt"
          },
          "tableOfContents": {
            "type": "string",
            "description": "Table of contents endpoint.",
            "nullable": true,
            "example": "/api/toc"
          },
          "swagger": {
            "type": "string",
            "description": "API Reference URL.",
            "nullable": true,
            "example": "/swagger"
          },
          "openapi": {
            "type": "string",
            "description": "OpenAPI specification URL.",
            "nullable": true,
            "example": "/swagger/v1/swagger.json"
          },
          "errors": {
            "type": "string",
            "description": "Error catalog endpoint.",
            "nullable": true,
            "example": "/api/errors"
          },
          "discovery": {
            "type": "string",
            "description": "Discovery endpoint.",
            "nullable": true,
            "example": "/api/discovery"
          },
          "batch": {
            "type": "string",
            "description": "Batch operations endpoint.",
            "nullable": true,
            "example": "/api/batch"
          },
          "websocket": {
            "type": "string",
            "description": "WebSocket hub URL.",
            "nullable": true,
            "example": "ws://localhost:5000/api/events"
          },
          "asyncApi": {
            "type": "string",
            "description": "Mounts the human-readable AsyncAPI viewer at /asyncapi for Layer 3 WebSocket event discovery.",
            "nullable": true,
            "example": "/asyncapi"
          }
        },
        "additionalProperties": false,
        "description": "Navigation links to key API endpoints and resources."
      },
      "ApiRootResponse": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "API name.",
            "nullable": true,
            "example": "OverMox API"
          },
          "version": {
            "type": "string",
            "description": "Semantic version of the API.",
            "nullable": true,
            "example": "1.0.0"
          },
          "description": {
            "type": "string",
            "description": "Short description of the API.",
            "nullable": true,
            "example": "REST API and WebSocket interface for the OverMox visual node graph editor"
          },
          "website": {
            "type": "string",
            "description": "Project website URL.",
            "nullable": true,
            "example": "https://overmox.com"
          },
          "documentation": {
            "type": "string",
            "description": "Documentation URL.",
            "nullable": true,
            "example": "https://overmox.com/docs"
          },
          "links": {
            "$ref": "#/components/schemas/ApiRootLinks"
          }
        },
        "additionalProperties": false,
        "description": "API discovery signpost with version info and navigation links."
      },
      "ApplyLayoutRequest": {
        "required": [
          "mode"
        ],
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "description": "Layout mode to apply (required). Valid values: Story, StoryOrganic, StorySeeded, Compact, CompactOrganic, CompactSeeded, Wide.",
            "nullable": true,
            "example": "Story"
          },
          "seed": {
            "type": "integer",
            "description": "Random seed for seeded layout modes (optional). Only used with StorySeeded and CompactSeeded.",
            "format": "int32",
            "nullable": true
          },
          "animate": {
            "type": "boolean",
            "description": "Whether to animate the transition to new positions (optional, default true)."
          },
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Specific node IDs to layout (optional). If null or empty, all nodes are laid out.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to apply an auto-layout algorithm to a graph."
      },
      "AspectRatioResponse": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "The aspect ratio as a decimal value (width / height).",
            "format": "double",
            "example": 1.3333333333333333
          },
          "display": {
            "type": "string",
            "description": "A human-readable display string for the aspect ratio.",
            "nullable": true,
            "example": "16:12"
          }
        },
        "additionalProperties": false,
        "description": "The current target aspect ratio for auto-layout."
      },
      "AssetPropertyListResponse": {
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "nullable": true
          },
          "assetType": {
            "type": "string",
            "nullable": true
          },
          "shader": {
            "type": "string",
            "nullable": true
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertyItem"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BatchOperation": {
        "required": [
          "method",
          "path"
        ],
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "description": "HTTP method: GET, POST, PUT, or DELETE.",
            "nullable": true,
            "example": "POST"
          },
          "path": {
            "type": "string",
            "description": "API path starting with /api/. Recursive /api/batch calls are blocked.",
            "nullable": true,
            "example": "/api/graphs"
          },
          "body": {
            "description": "Request body for POST/PUT operations. Ignored for GET/DELETE.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single operation within a batch request."
      },
      "BatchOperationResult": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Zero-based index of this operation in the batch.",
            "format": "int32",
            "example": 0
          },
          "method": {
            "type": "string",
            "description": "HTTP method that was executed.",
            "nullable": true,
            "example": "POST"
          },
          "path": {
            "type": "string",
            "description": "API path that was called.",
            "nullable": true,
            "example": "/api/graphs"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code returned by the operation.",
            "format": "int32",
            "example": 201
          },
          "body": {
            "description": "Response body from the operation, or an error envelope on failure.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Result of a single operation within a batch."
      },
      "BatchRequest": {
        "required": [
          "operations"
        ],
        "type": "object",
        "properties": {
          "operations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchOperation"
            },
            "description": "Array of operations to execute. Each runs independently. A failure stops only its own operation, not subsequent ones.",
            "nullable": true,
            "example": [
              {
                "method": "POST",
                "path": "/api/graphs",
                "body": {
                  "name": "MyGraph"
                }
              }
            ]
          }
        },
        "additionalProperties": false,
        "description": "A batch request containing multiple API operations to execute sequentially."
      },
      "BatchResponse": {
        "type": "object",
        "properties": {
          "totalOperations": {
            "type": "integer",
            "description": "Total number of operations in the batch.",
            "format": "int32",
            "example": 3
          },
          "succeeded": {
            "type": "integer",
            "description": "Number of operations that returned 2xx status.",
            "format": "int32",
            "example": 2
          },
          "failed": {
            "type": "integer",
            "description": "Number of operations that returned non-2xx status.",
            "format": "int32",
            "example": 1
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchOperationResult"
            },
            "description": "Per-operation results in execution order.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Response from a batch execution containing per-operation results."
      },
      "BuildApiError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code.",
            "nullable": true,
            "example": "NOT_FOUND"
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message.",
            "nullable": true,
            "example": "Graph with the specified ID was not found."
          },
          "details": {
            "description": "Optional additional details about the error (validation failures, stack traces, etc.).",
            "nullable": true
          },
          "build": {
            "$ref": "#/components/schemas/BuildErrorDetail"
          }
        },
        "additionalProperties": false,
        "description": "Extends OverMoxController.Api.DTOs.Common.ApiError with build-specific context."
      },
      "BuildConnectionSpec": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "Source port reference. Formats: \u0022tempId/PortName\u0022, \u0022tempId/output/PortName\u0022,\r\n\u0022{nodeGuid}/PortName\u0022, or \u0022{portGuid}\u0022.",
            "nullable": true
          },
          "to": {
            "type": "string",
            "description": "Destination port reference. Formats: \u0022tempId/PortName\u0022, \u0022tempId/input/PortName\u0022,\r\n\u0022{nodeGuid}/PortName\u0022, or \u0022{portGuid}\u0022.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Specification for a connection to create in the build request."
      },
      "BuildErrorDetail": {
        "type": "object",
        "properties": {
          "phase": {
            "type": "string",
            "description": "The build phase that failed (nodes, ports, connections).",
            "nullable": true,
            "example": "nodes"
          },
          "step": {
            "type": "integer",
            "description": "Zero-based index of the item within the phase that caused the failure.",
            "format": "int32",
            "example": 0
          },
          "detail": {
            "type": "string",
            "description": "Human-readable description of the failure (mirrors the top-level message).",
            "nullable": true
          },
          "rolledBack": {
            "type": "boolean",
            "description": "Whether previously created nodes were rolled back after the failure.",
            "example": false
          },
          "nodesRemoved": {
            "type": "integer",
            "description": "Number of nodes that were removed during rollback.",
            "format": "int32",
            "example": 0
          }
        },
        "additionalProperties": false,
        "description": "Describes where in the build pipeline an error occurred and whether rollback was performed."
      },
      "BuildErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/BuildApiError"
          }
        },
        "additionalProperties": false,
        "description": "Error response for graph build failures, extending the standard error envelope\r\nwith build-specific detail (phase, step, rollback status)."
      },
      "BuildGraphRequest": {
        "type": "object",
        "properties": {
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BuildNodeSpec"
            },
            "description": "Node specifications to create. Each must have a unique tempId and valid category.\r\nMaximum 1000 nodes per request.",
            "nullable": true
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BuildConnectionSpec"
            },
            "description": "Connections to create after all nodes and port values are set.\r\nFormat: \u0022tempId/PortName\u0022 for new nodes, \u0022{guid}/PortName\u0022 or \u0022{portGuid}\u0022 for existing.\r\nMaximum 5000 connections per request.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Declarative graph builder request. Creates nodes, sets port values, and wires connections in one call."
      },
      "BuildGraphResponse": {
        "type": "object",
        "properties": {
          "nodes": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/NodeDetailResponse"
            },
            "description": "Map of tempId to the created node\u0027s full detail response.",
            "nullable": true
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectionResponse"
            },
            "description": "All connections created by this build request.",
            "nullable": true
          },
          "portsSet": {
            "type": "integer",
            "description": "Number of port values successfully set across all nodes.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Successful graph builder response. Maps tempIds to created nodes and lists wired connections."
      },
      "BuildNodeSpec": {
        "type": "object",
        "properties": {
          "tempId": {
            "type": "string",
            "description": "Temporary identifier for this node, used to reference it in connections.\r\nMust be unique within the request.",
            "nullable": true
          },
          "category": {
            "type": "string",
            "description": "The Category enum value for the node type to create.\r\nUse GET /api/node-types/enum-values for valid values.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Optional display name for the node.",
            "nullable": true
          },
          "ports": {
            "type": "object",
            "additionalProperties": {},
            "description": "Optional port values to set after creation. Keys are port names (case-insensitive),\r\nvalues are the port values. Supports compound formats for vector ([x,y,z]) and color ([r,g,b,a]) ports.\r\nDropdown ports are set first to ensure dependent ports exist.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Specification for a single node to create in the build request."
      },
      "BulkCreateNodesRequest": {
        "required": [
          "nodes"
        ],
        "type": "object",
        "properties": {
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AddNodeRequest"
            },
            "description": "The list of node specifications to create (required, non-empty). Maximum 1000 entries.\r\nEach entry follows the same schema as the single-node POST endpoint.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to create multiple nodes in a single batch operation."
      },
      "BulkPropertySetRequest": {
        "required": [
          "properties"
        ],
        "type": "object",
        "properties": {
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertySetItem"
            },
            "description": "The {name, value} pairs to write on the component (required, non-empty).",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BulkPropertySetResponse": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "nullable": true
          },
          "component": {
            "type": "string",
            "nullable": true
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertySetResult"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "CategoryInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the category (e.g., \u0022scene\u0022, \u0022math\u0022, \u0022audio\u0022).\r\nUse this value when adding nodes via \u0060POST /api/graphs/{id}/nodes\u0060.",
            "nullable": true,
            "example": "scene"
          },
          "description": {
            "type": "string",
            "description": "Human-readable description of what this category contains.",
            "nullable": true,
            "example": "Manage scenes, objects, physics, transforms, and hierarchy"
          },
          "nodeCount": {
            "type": "integer",
            "description": "Number of node types available in this category.",
            "format": "int32",
            "example": 35
          },
          "nodeTypes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodeTypeInfo"
            },
            "description": "All node types available within this category.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Groups related node types under a named category for discovery."
      },
      "CategorySchemaResponse": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "The category name.",
            "nullable": true,
            "example": "scene"
          },
          "count": {
            "type": "integer",
            "description": "Number of actions in this category.",
            "format": "int32",
            "example": 5
          },
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActionParameterSchema"
            },
            "description": "Action schemas within this category.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Action schemas for a single category."
      },
      "ClipBinding": {
        "type": "object",
        "properties": {
          "node": {
            "type": "string",
            "description": "The target node name for a transform or morph binding; absent for a pointer binding.",
            "nullable": true
          },
          "path": {
            "type": "string",
            "description": "The channel path on the target node; absent for a pointer binding.",
            "nullable": true
          },
          "keyCount": {
            "type": "integer",
            "description": "The number of keyframes on this channel.",
            "format": "int32"
          },
          "morphNames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The named morph targets for a weights binding, when the mesh names them; absent otherwise.",
            "nullable": true
          },
          "pointer": {
            "type": "string",
            "description": "The JSON-pointer string for a pointer binding, or null when unavailable; absent for\r\ntransform and morph bindings.",
            "nullable": true
          },
          "constant": {
            "type": "boolean",
            "description": "True when every keyframe on this channel equals the first. Present only for transform and\r\nmorph bindings with a fresh capture; absent otherwise.",
            "nullable": true
          },
          "matchesRestPose": {
            "type": "boolean",
            "description": "True when every keyframe equals the node\u0027s rest pose. Present only for transform and morph\r\nbindings with a fresh capture; absent otherwise.",
            "nullable": true
          },
          "range": {
            "$ref": "#/components/schemas/ClipValueRange"
          }
        },
        "additionalProperties": false,
        "description": "Describes one animation channel: its target, key count, and optional value analysis."
      },
      "ClipIntrospectionResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the introspection call dispatched successfully."
          },
          "action": {
            "type": "string",
            "description": "The action string that produced this response (animation.introspectClip).",
            "nullable": true,
            "example": "animation.introspectClip"
          },
          "object": {
            "type": "string",
            "description": "The resolved scene object tag whose model was introspected.",
            "nullable": true
          },
          "clip": {
            "type": "string",
            "description": "The clip name that was introspected.",
            "nullable": true
          },
          "frame": {
            "type": "integer",
            "description": "The runtime frame on which the introspection ran.",
            "format": "int64"
          },
          "appliedAt": {
            "type": "number",
            "description": "The runtime time, in seconds, at which the introspection ran.",
            "format": "double"
          },
          "name": {
            "type": "string",
            "description": "The clip name as stored in the source model. Absent on an unavailable read.",
            "nullable": true
          },
          "source": {
            "type": "string",
            "description": "The lowercase source file extension without a dot (for example glb or gltf). Present on\r\nboth read shapes: on an unavailable read it is null, except the unsupported-format case,\r\nwhere it carries the rejected extension (for example fbx).",
            "nullable": true
          },
          "duration": {
            "type": "number",
            "description": "The clip length in seconds, read from the live imported clip. Absent on an unavailable read.",
            "format": "double",
            "nullable": true
          },
          "frameRate": {
            "type": "number",
            "description": "The clip sample rate, read from the live imported clip. Absent on an unavailable read.",
            "format": "double",
            "nullable": true
          },
          "wrapMode": {
            "type": "string",
            "description": "The clip wrap mode: loop, once, pingPong, clampForever, or default. Absent on an\r\nunavailable read.",
            "nullable": true
          },
          "rootMotion": {
            "type": "boolean",
            "description": "Classification only: true when any channel targets a scene-root node with a translation\r\nor rotation path. Absent on an unavailable read.",
            "nullable": true
          },
          "sourceStamp": {
            "$ref": "#/components/schemas/ClipSourceStamp"
          },
          "bindings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClipBinding"
            },
            "description": "One entry per animation channel, in channel order. Absent on an unavailable read.",
            "nullable": true
          },
          "constant": {
            "type": "boolean",
            "description": "True when every keyframe across the clip equals the first. Present only while the\r\nimport-time capture is fresh; absent otherwise.",
            "nullable": true
          },
          "matchesRestPose": {
            "type": "boolean",
            "description": "True when every keyframe equals the model\u0027s rest pose. Present only while the\r\nimport-time capture is fresh; absent otherwise.",
            "nullable": true
          },
          "introspection": {
            "type": "string",
            "description": "Set to \u0022unavailable\u0022 when the object\u0027s model has no readable source. Absent on a\r\nsuccessful read.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "A human-readable explanation of why introspection was unavailable. Absent on a\r\nsuccessful read.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries static introspection data for one animation clip, or an unavailable marker when\r\nthe source model cannot be read."
      },
      "ClipSourceStamp": {
        "type": "object",
        "properties": {
          "mtime": {
            "type": "string",
            "description": "The source file\u0027s last-modified time on disk.",
            "format": "date-time"
          },
          "size": {
            "type": "integer",
            "description": "The source file size in bytes.",
            "format": "int64"
          }
        },
        "additionalProperties": false,
        "description": "Records the source file\u0027s on-disk modification time and size as a staleness signal."
      },
      "ClipValueRange": {
        "type": "object",
        "properties": {
          "min": {
            "type": "array",
            "items": {
              "type": "number",
              "format": "float"
            },
            "description": "The per-component minimum values.",
            "nullable": true
          },
          "max": {
            "type": "array",
            "items": {
              "type": "number",
              "format": "float"
            },
            "description": "The per-component maximum values.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Holds the per-component minimum and maximum values observed across a channel."
      },
      "ComponentDetailResponse": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "nullable": true
          },
          "component": {
            "type": "string",
            "nullable": true
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertyItem"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ComponentListResponse": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "nullable": true
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ComponentSummary"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ComponentSummary": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "propertyCount": {
            "type": "integer",
            "format": "int32"
          },
          "url": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ConnectionResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of this connection.",
            "format": "uuid",
            "example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
          },
          "outputPortId": {
            "type": "string",
            "description": "The output port that data flows from.",
            "format": "uuid",
            "example": "b1c2d3e4-5678-9abc-def0-aabbccddeeff"
          },
          "inputPortId": {
            "type": "string",
            "description": "The input port that data flows into.",
            "format": "uuid",
            "example": "c2d3e4f5-6789-abcd-ef01-112233445566"
          },
          "outputNodeId": {
            "type": "string",
            "description": "The node that owns the output port.",
            "format": "uuid",
            "example": "a1b2c3d4-5678-9abc-def0-1234567890ab"
          },
          "inputNodeId": {
            "type": "string",
            "description": "The node that owns the input port.",
            "format": "uuid",
            "example": "d4e5f6a7-8901-bcde-f234-567890abcdef"
          }
        },
        "additionalProperties": false,
        "description": "Represents a connection (edge) between an output port and an input port in the graph."
      },
      "ConsoleEntryDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "The log message text.",
            "nullable": true,
            "example": "Node \u0022Add\u0022 executed successfully."
          },
          "type": {
            "type": "string",
            "description": "Log severity: \u0022log\u0022, \u0022warning\u0022, or \u0022error\u0022.",
            "nullable": true,
            "example": "log"
          },
          "contextType": {
            "type": "string",
            "description": "The kind of context that produced this log: \u0022graph\u0022, \u0022node\u0022, or \u0022none\u0022.",
            "nullable": true,
            "example": "node"
          },
          "contextId": {
            "type": "string",
            "description": "The unique identifier of the context (graph or node) that produced this log, if any.",
            "format": "uuid",
            "nullable": true
          },
          "contextName": {
            "type": "string",
            "description": "The display name of the context (graph or node) that produced this log, if any.",
            "nullable": true,
            "example": "Main Graph"
          },
          "timestamp": {
            "type": "string",
            "description": "UTC timestamp when the log entry was recorded.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "A single console log entry."
      },
      "CreateConnectionRequest": {
        "required": [
          "inputPortId",
          "outputPortId"
        ],
        "type": "object",
        "properties": {
          "outputPortId": {
            "type": "string",
            "description": "The output port to connect from (required).",
            "format": "uuid",
            "example": "b1c2d3e4-5678-9abc-def0-aabbccddeeff"
          },
          "inputPortId": {
            "type": "string",
            "description": "The input port to connect to (required).",
            "format": "uuid",
            "example": "c2d3e4f5-6789-abcd-ef01-112233445566"
          }
        },
        "additionalProperties": false,
        "description": "Request to create a connection between an output port and an input port."
      },
      "CreateGraphRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name for the new graph. Auto-generated if omitted.",
            "nullable": true,
            "example": "MainScene_Lighting"
          },
          "path": {
            "type": "string",
            "description": "Optional file system path where the graph asset should be created. Defaults to the project\u0027s default graph folder if omitted.",
            "nullable": true,
            "example": "Assets/Graphs/MainScene_Lighting.asset"
          }
        },
        "additionalProperties": false,
        "description": "Request to create a new graph in the project."
      },
      "CreateVariableRequest": {
        "required": [
          "name",
          "variableType"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name for the new variable (required).",
            "nullable": true,
            "example": "PlayerSpeed"
          },
          "variableType": {
            "type": "string",
            "description": "Data type of the variable (required). Must be a supported type (e.g., Float, String, Boolean, Integer, Vector3).",
            "nullable": true,
            "example": "Float"
          },
          "type": {
            "type": "string",
            "description": "Alias for OverMoxController.Api.DTOs.Variables.CreateVariableRequest.VariableType. If both are provided, \u0060variableType\u0060 takes precedence.",
            "nullable": true
          },
          "value": {
            "description": "Initial value. Optional. Defaults to the type default (0, \u0022\u0022, false, etc.).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to create a new graph variable."
      },
      "DiscoveryDataResponse": {
        "type": "object",
        "properties": {
          "dataType": {
            "type": "string",
            "description": "The data type that was requested.",
            "nullable": true,
            "example": "scene-objects"
          },
          "count": {
            "type": "integer",
            "description": "Number of items returned.",
            "format": "int32",
            "example": 12
          },
          "items": {
            "description": "The fetched data items. Shape depends on the data type.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Runtime data fetched from OverMox for a specific data type."
      },
      "DiscoveryMetaEntry": {
        "type": "object",
        "properties": {
          "dataType": {
            "type": "string",
            "description": "URL path segment for this data type (e.g., \u0022scene-objects\u0022, \u0022bundled-assets\u0022).",
            "nullable": true,
            "example": "scene-objects"
          },
          "description": {
            "type": "string",
            "description": "Human-readable description of what this data type contains.",
            "nullable": true,
            "example": "All objects currently in the OverMox scene"
          },
          "requiresOvermox": {
            "type": "boolean",
            "description": "Whether fetching this data type requires OverMox to be connected.",
            "example": true
          },
          "itemType": {
            "type": "string",
            "description": "The type of items returned in the response array.",
            "nullable": true,
            "example": "OverMoxRuntimeItem"
          }
        },
        "additionalProperties": false,
        "description": "Describes a single data type available through the discovery endpoint."
      },
      "DiscoveryMetaResponse": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "Description of the discovery endpoint and how to use it.",
            "nullable": true,
            "example": "Discover OverMox runtime data. Use GET /api/discovery/{dataType} to fetch data. Use ?filter= to narrow results for list types."
          },
          "dataTypeCount": {
            "type": "integer",
            "description": "Number of available data types.",
            "format": "int32",
            "example": 10
          },
          "dataTypes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DiscoveryMetaEntry"
            },
            "description": "All available data types with descriptions.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Metadata listing all available discovery data types."
      },
      "ErrorCatalogEntry": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "The machine-readable error code returned in error responses.",
            "nullable": true,
            "example": "GRAPH_NOT_FOUND"
          },
          "httpStatus": {
            "type": "integer",
            "description": "The HTTP status code associated with this error.",
            "format": "int32",
            "example": 404
          },
          "description": {
            "type": "string",
            "description": "A human-readable description of when this error occurs.",
            "nullable": true,
            "example": "The specified graph ID does not exist in the current project."
          },
          "resolution": {
            "type": "string",
            "description": "Suggested steps to resolve this error.",
            "nullable": true,
            "example": "Use GET /api/graphs to list available graphs."
          }
        },
        "additionalProperties": false,
        "description": "Describes a single API error code, its HTTP status, and how to resolve it."
      },
      "ErrorCatalogResponse": {
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ErrorCatalogEntry"
            },
            "description": "All known error codes, their HTTP statuses, and resolution guidance.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Response containing the full catalog of API error codes."
      },
      "ExportFailureDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The node name that failed to render.",
            "nullable": true,
            "example": "Mystery"
          },
          "reason": {
            "type": "string",
            "description": "Short reason describing why the render failed.",
            "nullable": true,
            "example": "Unknown node name."
          }
        },
        "additionalProperties": false,
        "description": "A single failed export entry with a short reason string."
      },
      "ExportPngsRequest": {
        "type": "object",
        "properties": {
          "nodeNames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of nodes to render. Null or empty means render every node.",
            "nullable": true,
            "example": [
              "LogicGate",
              "GetLayer"
            ]
          }
        },
        "additionalProperties": false,
        "description": "Request body for the dev-only PNG export endpoint."
      },
      "ExportPngsResponse": {
        "type": "object",
        "properties": {
          "exported": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of nodes that were rendered successfully and written to every output directory.",
            "nullable": true
          },
          "failed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExportFailureDto"
            },
            "description": "Per-node failure entries. Empty when every requested name rendered cleanly.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Result of a PNG export run: lists of node names that succeeded and failed."
      },
      "GraphDetailResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the graph.",
            "format": "uuid",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "name": {
            "type": "string",
            "description": "Human-readable name of the graph.",
            "nullable": true,
            "example": "MainScene_Lighting"
          },
          "path": {
            "type": "string",
            "description": "File system path to the graph asset, relative to the project root.",
            "nullable": true,
            "example": "Assets/Graphs/MainScene_Lighting.asset"
          },
          "isRunning": {
            "type": "boolean",
            "description": "Whether the graph is currently executing in OverMox."
          },
          "hasUnsavedChanges": {
            "type": "boolean",
            "description": "Whether the graph has been modified since the last save."
          },
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodeDetailResponse"
            },
            "description": "All nodes contained in this graph.",
            "nullable": true
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectionResponse"
            },
            "description": "All connections between nodes in this graph.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full detail view of a graph including its nodes and connections."
      },
      "GraphListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the graph.",
            "format": "uuid",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "name": {
            "type": "string",
            "description": "Human-readable name of the graph.",
            "nullable": true,
            "example": "MainScene_Lighting"
          },
          "path": {
            "type": "string",
            "description": "File system path to the graph asset, relative to the project root.",
            "nullable": true,
            "example": "Assets/Graphs/MainScene_Lighting.asset"
          },
          "isRunning": {
            "type": "boolean",
            "description": "Whether the graph is currently executing in OverMox."
          },
          "hasUnsavedChanges": {
            "type": "boolean",
            "description": "Whether the graph has been modified since the last save."
          },
          "nodeCount": {
            "type": "integer",
            "description": "Total number of nodes in the graph.",
            "format": "int32",
            "example": 12
          },
          "connectionCount": {
            "type": "integer",
            "description": "Total number of connections between nodes in the graph.",
            "format": "int32",
            "example": 8
          }
        },
        "additionalProperties": false,
        "description": "Represents a graph in the project list with summary information."
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Overall health status of the API.",
            "nullable": true,
            "example": "healthy"
          },
          "overmoxConnected": {
            "type": "boolean",
            "description": "Whether the API is currently connected to a running OverMox instance."
          },
          "projectName": {
            "type": "string",
            "description": "Name of the currently loaded OverMox project, if any.",
            "nullable": true,
            "example": "MyOverMoxProject"
          },
          "apiVersion": {
            "type": "string",
            "description": "Semantic version of the OverMox API.",
            "nullable": true,
            "example": "1.0.0"
          }
        },
        "additionalProperties": false,
        "description": "Reports the current health status of the OverMox API and its OverMox connection."
      },
      "ImportGraphRequest": {
        "required": [
          "path"
        ],
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Absolute file system path to the .ovgraph file to import (required).",
            "nullable": true,
            "example": "C:/Projects/MyOverMoxProject/Graphs/DonationGoal.ovgraph"
          }
        },
        "additionalProperties": false,
        "description": "Request to import an existing graph from a file system path."
      },
      "Layer1FieldSchema": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "The field\u0027s type.",
            "nullable": true,
            "example": "string"
          },
          "required": {
            "type": "boolean",
            "description": "True when the field must be supplied on the request."
          },
          "description": {
            "type": "string",
            "description": "A description of the field and its accepted values.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one request field: its type, whether it is required, and its purpose."
      },
      "Layer1Operation": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "description": "The HTTP method the operation uses.",
            "nullable": true,
            "example": "POST"
          },
          "path": {
            "type": "string",
            "description": "The route the operation targets.",
            "nullable": true,
            "example": "/api/graphs/{graphId}/nodes"
          },
          "request": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Layer1FieldSchema"
            },
            "description": "The request fields keyed by field name; an empty object when the shape could not be derived.",
            "nullable": true
          },
          "response": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "The response shape keyed by response DTO name, with the value \u0022see DTO source\u0022; an empty\r\nobject when the operation declares no response DTO.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "A note about the operation, such as where to find an undeclared response shape; an empty\r\nstring when there is no note.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one Layer 1 operation: its route, request fields, response shape, and notes."
      },
      "Layer1SchemasResponse": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "A short description of the schema catalog and how to use it.",
            "nullable": true
          },
          "operations": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Layer1Operation"
            },
            "description": "The operations keyed by camelCase operation id, such as addNode.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries the request and response schemas for the Layer 1 CRUD operations."
      },
      "LayoutResultResponse": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "description": "The layout mode that was applied (or \u0022TidyUp\u0022 / \u0022Restore\u0022 for those operations).",
            "nullable": true
          },
          "nodePositions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodePositionItem"
            },
            "description": "The new positions of all affected nodes after layout.",
            "nullable": true
          },
          "aspectRatio": {
            "type": "number",
            "description": "The current target aspect ratio used by the layout engine.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Result of a layout operation, including the new positions of all affected nodes."
      },
      "NodeDetailResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the node.",
            "format": "uuid",
            "example": "a1b2c3d4-5678-9abc-def0-1234567890ab"
          },
          "graphId": {
            "type": "string",
            "description": "The unique identifier of the parent graph that contains this node.\r\nNull when returned from graph-scoped endpoints where the graph ID is already in the URL.",
            "format": "uuid",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Display name of the node.",
            "nullable": true,
            "example": "Load Scene"
          },
          "category": {
            "type": "string",
            "description": "The node type category this node belongs to.",
            "nullable": true,
            "example": "scene"
          },
          "locationX": {
            "type": "number",
            "description": "Horizontal position of the node on the graph canvas.",
            "format": "double",
            "example": 350.0
          },
          "locationY": {
            "type": "number",
            "description": "Vertical position of the node on the graph canvas.",
            "format": "double",
            "example": 200.0
          },
          "inputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortValueResponse"
            },
            "description": "All input ports on this node.",
            "nullable": true
          },
          "outputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortValueResponse"
            },
            "description": "All output ports on this node.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full detail view of a node including all its input and output ports."
      },
      "NodePositionItem": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string",
            "description": "The node\u0027s unique identifier.",
            "format": "uuid"
          },
          "locationX": {
            "type": "number",
            "description": "The node\u0027s new X position on the canvas.",
            "format": "double"
          },
          "locationY": {
            "type": "number",
            "description": "The node\u0027s new Y position on the canvas.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "A node\u0027s position after layout."
      },
      "NodeTypeInfo": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "The category identifier used when adding a node of this type.",
            "nullable": true,
            "example": "scene"
          },
          "displayName": {
            "type": "string",
            "description": "Human-readable display name shown in the node editor UI.",
            "nullable": true,
            "example": "Load Scene"
          },
          "description": {
            "type": "string",
            "description": "Brief one-line explanation of what this node does.",
            "nullable": true,
            "example": "Loads a different scene by name"
          },
          "technicalDescription": {
            "type": "string",
            "description": "Detailed technical description of the node\u0027s behavior and implementation.",
            "nullable": true
          },
          "perfectFor": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "List of ideal use cases for this node.",
            "nullable": true
          },
          "inputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortSchema"
            },
            "description": "Input port definitions for this node type.",
            "nullable": true
          },
          "outputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortSchema"
            },
            "description": "Output port definitions for this node type.",
            "nullable": true
          },
          "actionString": {
            "type": "string",
            "description": "The Layer 2 action string that corresponds to this node\u0027s functionality, if any.\r\nUse this to cross-reference with \u0060GET /api/actions/schema/{category}/{action}\u0060.",
            "nullable": true,
            "example": "scene.switch"
          },
          "actionEndpoint": {
            "type": "string",
            "description": "The full Layer 2 REST endpoint path for this node\u0027s direct action, if any.",
            "nullable": true,
            "example": "/api/actions/scene/switch"
          },
          "actionMethod": {
            "type": "string",
            "description": "The HTTP method to use when calling OverMoxController.Api.DTOs.Discovery.NodeTypeInfo.ActionEndpoint (e.g., \u0022POST\u0022 for\r\nan action-bus call, \u0022GET\u0022 for a read such as an animation playback query), if any.",
            "nullable": true,
            "example": "POST"
          },
          "tips": {
            "type": "string",
            "description": "Best practices and usage tips for this node.",
            "nullable": true
          },
          "enumValue": {
            "type": "string",
            "description": "The exact Category enum value to use when adding this node via POST /api/graphs/{id}/nodes.\r\nPass this value as the \u0022category\u0022 field in the AddNode request.",
            "nullable": true,
            "example": "ConsoleLog"
          },
          "family": {
            "type": "string",
            "description": "Key of the node family this node belongs to, one of the keys in the catalog\u0027s family\r\ntable. Null when the catalog carries no family for this node.",
            "nullable": true,
            "example": "Scene"
          },
          "subheading": {
            "type": "string",
            "description": "Optional group inside the family, for example \u0022Arrays\u0022 inside \u0022Variables and arrays\u0022.\r\nNull when the node sits directly under its family.",
            "nullable": true,
            "example": "Arrays"
          },
          "displayLabel": {
            "type": "string",
            "description": "The short label to show in a node picker, with the family and subheading prefix words\r\ndropped. Falls back to OverMoxController.Api.DTOs.Discovery.NodeTypeInfo.DisplayName when the catalog carries none.",
            "nullable": true,
            "example": "Set Active"
          },
          "original": {
            "type": "string",
            "description": "The node\u0027s path in the retired hand-written node menu. Kept so a search for the old\r\nwording still finds the node; it is never displayed.",
            "nullable": true,
            "example": "Scene / Scene Objects / Set Active"
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Extra search terms for this node. Null when the catalog carries none.",
            "nullable": true
          },
          "deprecated": {
            "type": "boolean",
            "description": "True when the node is retired and must not be offered in a picker. Null or false means\r\nthe node is offered.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes an available node type that can be instantiated in a graph.\r\nContains full metadata including descriptions, port schemas, use cases, and action endpoint cross-references."
      },
      "NoteStateResponse": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string",
            "description": "The Note node\u0027s unique identifier.",
            "format": "uuid"
          },
          "text": {
            "type": "string",
            "description": "The note\u0027s text body. Supports curly-brace variable interpolation (e.g. \u0022{myVar}\u0022) at runtime.",
            "nullable": true,
            "example": "Reminder: tweak the curve before shipping"
          },
          "font": {
            "type": "string",
            "description": "The selected font family name. Must be one of OverMoxController.Api.DTOs.Notes.NoteStateResponse.AvailableFonts.",
            "nullable": true,
            "example": "Segoe UI"
          },
          "color": {
            "type": "string",
            "description": "The selected text color preset name. Must be one of OverMoxController.Api.DTOs.Notes.NoteStateResponse.AvailableColors.",
            "nullable": true,
            "example": "Gold"
          },
          "fontSize": {
            "type": "number",
            "description": "The selected font size. Must be one of OverMoxController.Api.DTOs.Notes.NoteStateResponse.AvailableFontSizes.",
            "format": "double",
            "example": 20
          },
          "availableFonts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Available font family names (system-installed fonts).",
            "nullable": true
          },
          "availableColors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Available text color preset names. The note\u0027s color must be set to one of these by name.",
            "nullable": true
          },
          "availableFontSizes": {
            "type": "array",
            "items": {
              "type": "number",
              "format": "double"
            },
            "description": "Available font sizes. The note\u0027s fontSize must be set to one of these.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Current state of a Note node plus the available options for each formatting field."
      },
      "OpenProjectRequest": {
        "required": [
          "path"
        ],
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Absolute file system path to the project directory to open (required).",
            "nullable": true,
            "example": "C:/Projects/MyOverMoxProject"
          },
          "discardUnsaved": {
            "type": "boolean",
            "description": "Set true to discard unsaved scene changes and continue the open (optional, defaults to false).",
            "example": false
          }
        },
        "additionalProperties": false,
        "description": "Request to open an OverMox project from the file system."
      },
      "PortOptionEntry": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "The zero-based position of the option in the list; pass it as the value to PUT /api/ports/{portId}/value.",
            "format": "int32"
          },
          "label": {
            "type": "string",
            "description": "The option\u0027s display label; pass it as the value to PUT /api/ports/{portId}/value.",
            "nullable": true
          },
          "key": {
            "type": "string",
            "description": "The option\u0027s data key, such as a property member path. Present only on Dropdown ports whose\r\noption data is a string; absent for Enum ports and keyless Dropdown entries.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one selectable option on a Dropdown or Enum port."
      },
      "PortOptionsResponse": {
        "type": "object",
        "properties": {
          "portId": {
            "type": "string",
            "description": "The unique identifier of the inspected port.",
            "format": "uuid"
          },
          "portName": {
            "type": "string",
            "description": "The display name of the inspected port.",
            "nullable": true
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortOptionEntry"
            },
            "description": "One entry per selectable option, in the order the port presents them.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries the discoverable options on a Dropdown or Enum port, with the port\u0027s identity."
      },
      "PortSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the port as it appears on the node.",
            "nullable": true,
            "example": "Intensity"
          },
          "type": {
            "type": "string",
            "description": "Data type of the port (e.g., Float, String, Boolean, Integer, Vector3, Color, Trigger, SceneObject, Dropdown, Any).\r\nSee \u0060GET /api/types\u0060 for the full list of valid port types and compatibility rules.",
            "nullable": true,
            "example": "Float"
          },
          "isInput": {
            "type": "boolean",
            "description": "Whether this is an input port (true) or output port (false)."
          },
          "description": {
            "type": "string",
            "description": "Human-readable description of what this port does.",
            "nullable": true,
            "example": "How strong the explosion push is"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Valid values for Enum and Dropdown port types. Null for all other types.",
            "nullable": true
          },
          "dynamic": {
            "type": "boolean",
            "description": "True if the dropdown options are populated dynamically at runtime (e.g., variable lists).\r\nWhen true, the Options array will be empty. Use the Note field for guidance.",
            "nullable": true
          },
          "note": {
            "type": "string",
            "description": "Explanation of what populates a dynamic dropdown, or additional port usage guidance.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes a port definition on a node type, used for discovery and introspection."
      },
      "PortValueResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the port.",
            "format": "uuid",
            "example": "b1c2d3e4-5678-9abc-def0-aabbccddeeff"
          },
          "name": {
            "type": "string",
            "description": "Display name of the port.",
            "nullable": true,
            "example": "Intensity"
          },
          "type": {
            "type": "string",
            "description": "Data type of the port (e.g., Float, String, Boolean, Integer, Vector3).",
            "nullable": true,
            "example": "Float"
          },
          "tag": {
            "type": "string",
            "description": "Optional tag used for port categorization or filtering.",
            "nullable": true,
            "example": "lighting"
          },
          "isInput": {
            "type": "boolean",
            "description": "Whether this is an input port (true) or output port (false)."
          },
          "isConnected": {
            "type": "boolean",
            "description": "Whether this port is currently connected to another port."
          },
          "value": {
            "description": "The current value of the port. Type depends on the port\u0027s data type.\r\nFor sensitive ports, this is null when redacted (see isSensitive).",
            "nullable": true,
            "example": 0.75
          },
          "isSensitive": {
            "type": "boolean",
            "description": "Whether this port contains sensitive data (e.g., JWT tokens, API keys).\r\nWhen true and the Controller\u0027s \u0022Expose sensitive port values\u0022 setting is disabled,\r\nthe value is redacted (null) and hasValue indicates whether a value is set.\r\nTo enable API access to sensitive values, enable the setting in the Controller UI."
          },
          "hasValue": {
            "type": "boolean",
            "description": "For sensitive ports only: indicates whether a value has been set, even though\r\nthe actual value is redacted. Null (omitted from JSON) when not applicable.",
            "nullable": true
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Available options for Dropdown/Enum ports. Null for other port types.\r\nFor variable selectors, lists available variable names. Use the name string\r\n(not the index) when setting the value via PUT /api/ports/{id}/value.",
            "nullable": true
          },
          "isExpandable": {
            "type": "boolean",
            "description": "True if this port can be expanded into sub-ports (Vector-\u003EX/Y/Z, Color-\u003ER/G/B/A).\r\nOmitted for non-expandable ports.",
            "nullable": true
          },
          "isExpanded": {
            "type": "boolean",
            "description": "Current expansion state. True if expanded, false if collapsed.\r\nOmitted for non-expandable ports.",
            "nullable": true
          },
          "isExpandedChild": {
            "type": "boolean",
            "description": "True if this port is a sub-port created by expansion (e.g., the X port from an expanded Vector).\r\nOmitted for regular ports.",
            "nullable": true
          },
          "parentPortId": {
            "type": "string",
            "description": "The ID of the parent port that was expanded to create this sub-port.\r\nOmitted for regular ports.",
            "format": "uuid",
            "nullable": true
          },
          "affectsNodePorts": {
            "type": "boolean",
            "description": "True if changing this port\u0027s value may add, remove, or change other ports on the node.\r\nWhen true, use the returned node data to refresh cached port IDs.\r\nOmitted for ports that don\u0027t affect node structure.",
            "nullable": true
          },
          "isDisabled": {
            "type": "boolean",
            "description": "Whether this port is currently disabled (ignored during graph execution).\r\nAlways present on every port response. Defaults to false.\r\nCurrently only trigger ports support being toggled via\r\nPOST /api/ports/{id}/disable and POST /api/ports/{id}/enable; other\r\nport types surface their existing ignore state but cannot be toggled."
          }
        },
        "additionalProperties": false,
        "description": "Represents a port on a node with its current value and metadata."
      },
      "ProjectInfoResponse": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the project.",
            "nullable": true,
            "example": "MyOverMoxProject"
          },
          "path": {
            "type": "string",
            "description": "Absolute file system path to the project directory.",
            "nullable": true,
            "example": "C:/Projects/MyOverMoxProject"
          },
          "isOpen": {
            "type": "boolean",
            "description": "Whether a project is currently open."
          },
          "isConnectedToOvermox": {
            "type": "boolean",
            "description": "Whether the controller is connected to a running OverMox instance."
          },
          "graphCount": {
            "type": "integer",
            "description": "Number of graphs in the project.",
            "format": "int32",
            "example": 5
          }
        },
        "additionalProperties": false,
        "description": "Information about the currently loaded OverMox project."
      },
      "PropertyItem": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "key": {
            "type": "string",
            "nullable": true
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "nullable": true
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "structural": {
            "type": "boolean"
          },
          "url": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PropertyResponse": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "nullable": true
          },
          "component": {
            "type": "string",
            "nullable": true
          },
          "property": {
            "type": "string",
            "nullable": true
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PropertySetItem": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PropertySetResult": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "previousValue": {
            "nullable": true
          },
          "value": {
            "nullable": true
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PropertyWriteRequest": {
        "type": "object",
        "properties": {
          "value": {
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PropertyWriteResponse": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "nullable": true
          },
          "component": {
            "type": "string",
            "nullable": true
          },
          "property": {
            "type": "string",
            "nullable": true
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "previousValue": {
            "nullable": true
          },
          "value": {
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "RestorePositionsRequest": {
        "type": "object",
        "properties": {
          "animate": {
            "type": "boolean",
            "description": "Whether to animate the transition back to saved positions (optional, default true)."
          }
        },
        "additionalProperties": false,
        "description": "Request to restore node positions to their pre-layout state."
      },
      "RootSignpostResponse": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Application name.",
            "nullable": true,
            "example": "OverMox Controller"
          },
          "api": {
            "type": "string",
            "description": "Path to the API root.",
            "nullable": true,
            "example": "/api"
          },
          "health": {
            "type": "string",
            "description": "Path to the health check endpoint.",
            "nullable": true,
            "example": "/api/health"
          }
        },
        "additionalProperties": false,
        "description": "Minimal signpost returned at the server root, pointing to the API and health check."
      },
      "RuntimeContextCamera": {
        "required": [
          "name",
          "tag"
        ],
        "type": "object",
        "properties": {
          "tag": {
            "type": "string",
            "description": "The camera scene object tag, matching the tag published by the camera.changed event.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "The camera scene object name, matching the name published by the camera.changed event.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Identifies the resolved main camera by the same tag and name the camera.changed event carries."
      },
      "RuntimeContextProject": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The project display name, derived from the project folder name, and empty rather than\r\nnull when the runtime reports no path.",
            "nullable": true,
            "example": "MyShow"
          },
          "path": {
            "type": "string",
            "description": "The project folder path on disk, or null when the runtime reports none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Identifies the open project by its display name and its location on disk."
      },
      "RuntimeContextResolution": {
        "required": [
          "resolutionX",
          "resolutionY"
        ],
        "type": "object",
        "properties": {
          "resolutionX": {
            "type": "integer",
            "description": "The output width in pixels.",
            "format": "int32",
            "example": 1920
          },
          "resolutionY": {
            "type": "integer",
            "description": "The output height in pixels.",
            "format": "int32",
            "example": 1080
          }
        },
        "additionalProperties": false,
        "description": "Reports the output resolution in pixels."
      },
      "RuntimeContextResponse": {
        "type": "object",
        "properties": {
          "project": {
            "$ref": "#/components/schemas/RuntimeContextProject"
          },
          "scene": {
            "$ref": "#/components/schemas/RuntimeContextScene"
          },
          "camera": {
            "$ref": "#/components/schemas/RuntimeContextCamera"
          },
          "resolution": {
            "$ref": "#/components/schemas/RuntimeContextResolution"
          }
        },
        "additionalProperties": false,
        "description": "Carries one snapshot of the live runtime context: the open project, the active scene,\r\nthe resolved main camera, and the output resolution."
      },
      "RuntimeContextScene": {
        "required": [
          "name",
          "tag"
        ],
        "type": "object",
        "properties": {
          "tag": {
            "type": "string",
            "description": "The scene tag, matching the tag published by the scene.activated event.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "The scene asset name, matching the name published by the scene.activated event.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Identifies the active scene by the same tag and name the scene.activated event carries."
      },
      "SavedPositionsResponse": {
        "type": "object",
        "properties": {
          "hasSavedPositions": {
            "type": "boolean",
            "description": "True if the graph has saved positions that can be restored."
          }
        },
        "additionalProperties": false,
        "description": "Whether saved (pre-layout) positions exist for a graph."
      },
      "SetAspectRatioRequest": {
        "required": [
          "value"
        ],
        "type": "object",
        "properties": {
          "value": {
            "description": "The aspect ratio value. Accepts a decimal number (e.g. 1.33) or a ratio string (e.g. \u002216:9\u0022, \u002216:12\u0022).",
            "nullable": true,
            "example": 1.33
          }
        },
        "additionalProperties": false,
        "description": "Request to set the target aspect ratio for auto-layout."
      },
      "SetPortTypeRequest": {
        "required": [
          "type"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "The new data type for the port (required). Must be a supported port type (e.g., Float, String, Boolean, Integer, Vector3).",
            "nullable": true,
            "example": "Float"
          }
        },
        "additionalProperties": false,
        "description": "Request to change the data type of a port."
      },
      "SetPortValueRequest": {
        "required": [
          "value"
        ],
        "type": "object",
        "properties": {
          "value": {
            "description": "The new value to assign to the port (required). Must be compatible with the port\u0027s data type.",
            "nullable": true,
            "example": 0.75
          }
        },
        "additionalProperties": false,
        "description": "Request to set the value of a port. The value type must match the port\u0027s data type."
      },
      "SpawnableEntry": {
        "type": "object",
        "properties": {
          "menuPath": {
            "type": "string",
            "description": "The catalog menu path that feeds the \u0027type\u0027 parameter on POST /api/actions/scene/spawn.",
            "nullable": true,
            "example": "Simple/Cube"
          },
          "toolTip": {
            "type": "string",
            "description": "The catalog entry\u0027s tooltip text; an empty string when the entry carries none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Describes one spawnable catalog Element: its menu path and tooltip."
      },
      "SpawnablesResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SpawnableEntry"
            },
            "description": "One entry per spawnable catalog Element; empty when the project defines none.",
            "nullable": true
          },
          "count": {
            "type": "integer",
            "description": "The number of entries in Items.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Catalogs the project\u0027s spawnable Elements as menuPath and toolTip pairs."
      },
      "TidyUpRequest": {
        "type": "object",
        "properties": {
          "animate": {
            "type": "boolean",
            "description": "Whether to animate the transition to new positions (optional, default true)."
          },
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Specific node IDs to tidy (optional). If null or empty, all nodes are tidied.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to tidy up node positions (grid-snap and alignment)."
      },
      "TocEndpointEntry": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "description": "HTTP method (GET, POST, PUT, DELETE, WS).",
            "nullable": true,
            "example": "GET"
          },
          "path": {
            "type": "string",
            "description": "URL path for the endpoint.",
            "nullable": true,
            "example": "/api/health"
          },
          "purpose": {
            "type": "string",
            "description": "One-line description of what the endpoint does.",
            "nullable": true,
            "example": "Check API status and OverMox connection"
          }
        },
        "additionalProperties": false,
        "description": "A single entry in the API table of contents."
      },
      "TocResponse": {
        "type": "object",
        "properties": {
          "apiVersion": {
            "type": "string",
            "description": "Current API version.",
            "nullable": true,
            "example": "1.0.0"
          },
          "endpointCount": {
            "type": "integer",
            "description": "Total number of endpoints listed.",
            "format": "int32",
            "example": 45
          },
          "endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TocEndpointEntry"
            },
            "description": "Flat list of every API endpoint.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Table of contents listing every API endpoint with method, path, and purpose."
      },
      "TypeCatalogResponse": {
        "type": "object",
        "properties": {
          "portTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "All valid port type names.",
            "nullable": true,
            "example": [
              "Array",
              "Boolean",
              "Button",
              "Color",
              "Dropdown",
              "Empty",
              "Enum",
              "Float",
              "Trigger",
              "Generic",
              "Integer",
              "Note",
              "ObjectSelector",
              "String",
              "Text",
              "Vector3"
            ]
          },
          "variableTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "All valid variable type names.",
            "nullable": true,
            "example": [
              "Boolean",
              "Float",
              "Integer",
              "String",
              "Vector3"
            ]
          },
          "nodeCategories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "All valid node category names.",
            "nullable": true,
            "example": [
              "scene",
              "math",
              "variables",
              "web",
              "flow",
              "triggers",
              "animations",
              "misc",
              "components",
              "text",
              "audio",
              "asset",
              "generators",
              "camera",
              "process",
              "converters",
              "file",
              "hardware",
              "input",
              "general"
            ]
          },
          "portTypeDescriptions": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "Human-readable description of each port type and its value semantics.",
            "nullable": true
          },
          "portValueFormats": {
            "type": "object",
            "additionalProperties": {
              "nullable": true
            },
            "description": "Example value formats for each settable port type.",
            "nullable": true
          },
          "connectionCompatibility": {
            "type": "string",
            "description": "Guidance on how to check connection compatibility between ports.",
            "nullable": true,
            "example": "Use POST /api/graphs/{id}/connections/validate to check if two ports can be connected. Compatibility depends on port types and context."
          }
        },
        "additionalProperties": false,
        "description": "Comprehensive catalog of all valid types, categories, and compatibility rules used across the API."
      },
      "UpdateGraphRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New name for the graph (required).",
            "nullable": true,
            "example": "MainScene_Lighting_v2"
          }
        },
        "additionalProperties": false,
        "description": "Request to update an existing graph\u0027s properties."
      },
      "UpdateNodeRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New display name for the node (optional).",
            "nullable": true,
            "example": "Load Scene (Main)"
          },
          "locationX": {
            "type": "number",
            "description": "New horizontal position on the graph canvas (optional).",
            "format": "double",
            "nullable": true,
            "example": 400.0
          },
          "locationY": {
            "type": "number",
            "description": "New vertical position on the graph canvas (optional).",
            "format": "double",
            "nullable": true,
            "example": 250.0
          }
        },
        "additionalProperties": false,
        "description": "Request to update an existing node\u0027s display name or position. All fields are optional; only provided fields are updated."
      },
      "UpdateNoteRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "New text body for the note (optional).",
            "nullable": true,
            "example": "Updated reminder text"
          },
          "font": {
            "type": "string",
            "description": "New font family name (optional). Must be one of the names from\r\nGET /api/graphs/{graphId}/nodes/{nodeId}/note availableFonts.",
            "nullable": true,
            "example": "Segoe UI"
          },
          "color": {
            "type": "string",
            "description": "New text color preset name (optional). Must be one of the names from\r\nGET /api/graphs/{graphId}/nodes/{nodeId}/note availableColors.",
            "nullable": true,
            "example": "Gold"
          },
          "fontSize": {
            "type": "number",
            "description": "New font size (optional). Must be one of the values from\r\nGET /api/graphs/{graphId}/nodes/{nodeId}/note availableFontSizes.",
            "format": "double",
            "nullable": true,
            "example": 20
          }
        },
        "additionalProperties": false,
        "description": "Partial update for a Note node. All fields are optional; only fields you set are applied."
      },
      "UpdateVariableRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New display name for the variable (optional).",
            "nullable": true,
            "example": "PlayerMaxSpeed"
          },
          "value": {
            "description": "New value for the variable (optional). Must be compatible with the variable\u0027s data type.",
            "nullable": true,
            "example": 10.0
          }
        },
        "additionalProperties": false,
        "description": "Request to update an existing graph variable. All fields are optional; only provided fields are updated."
      },
      "ValidateConnectionResponse": {
        "type": "object",
        "properties": {
          "isValid": {
            "type": "boolean",
            "description": "Whether the proposed connection is valid."
          },
          "reason": {
            "type": "string",
            "description": "Explanation of why the connection is invalid. Null when IsValid is true.",
            "nullable": true,
            "example": "Cannot connect ports of incompatible types: Float to String."
          }
        },
        "additionalProperties": false,
        "description": "Result of a connection validation check, indicating whether two ports can be connected."
      },
      "VariableDetailResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the variable.",
            "format": "uuid",
            "example": "e5f6a7b8-9012-cdef-3456-7890abcdef01"
          },
          "name": {
            "type": "string",
            "description": "Display name of the variable.",
            "nullable": true,
            "example": "PlayerSpeed"
          },
          "variableType": {
            "type": "string",
            "description": "Data type of the variable (e.g., Float, String, Boolean, Integer, Vector3).",
            "nullable": true,
            "example": "Float"
          },
          "value": {
            "description": "The current value of the variable. Type depends on VariableType.",
            "nullable": true,
            "example": 5.5
          }
        },
        "additionalProperties": false,
        "description": "Full detail view of a graph variable including its current value."
      },
      "VariableListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The unique identifier of the variable.",
            "format": "uuid",
            "example": "e5f6a7b8-9012-cdef-3456-7890abcdef01"
          },
          "name": {
            "type": "string",
            "description": "Display name of the variable.",
            "nullable": true,
            "example": "PlayerSpeed"
          },
          "variableType": {
            "type": "string",
            "description": "Data type of the variable (e.g., Float, String, Boolean, Integer, Vector3).",
            "nullable": true,
            "example": "Float"
          },
          "value": {
            "description": "The current value of the variable. Type depends on VariableType.",
            "nullable": true,
            "example": 5.5
          }
        },
        "additionalProperties": false,
        "description": "Summary representation of a graph variable."
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "description": "API key sent in the X-API-Key header.",
        "name": "X-API-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Dev",
      "description": "Internal and debug endpoints. Not for production use."
    },
    {
      "name": "Discovery",
      "description": "Discovery and metadata endpoints. Health, table of contents, schemas, type lists, and llms.txt. Use these to onboard agents and inspect the API surface."
    },
    {
      "name": "L1: Assets",
      "description": "Layer 1 property reads on bundled assets in the project."
    },
    {
      "name": "L1: Batch",
      "description": "Layer 1 batched-operation entry point. Bundle several Layer 1 mutations into a single round trip."
    },
    {
      "name": "L1: Console",
      "description": "Layer 1 console log reads. Inspect recent log lines emitted by the OverMox runtime."
    },
    {
      "name": "L1: Graphs",
      "description": "Layer 1 REST mutations on node graphs. Create, read, update, remove, duplicate, run, stop, sync, save, and import graphs. Use these to manage graph lifecycle."
    },
    {
      "name": "L1: Nodes",
      "description": "Layer 1 REST mutations on nodes within a graph. Add, read, update, and delete nodes. Use these when editing a graph\u0027s contents."
    },
    {
      "name": "L1: Objects",
      "description": "Layer 1 property reads on scene objects in the loaded project."
    },
    {
      "name": "L1: Ports",
      "description": "Layer 1 REST mutations on node ports. Use these to update port values from outside a running graph."
    },
    {
      "name": "L1: Project",
      "description": "Layer 1 project lifecycle. Open, close, and save the OverMox project."
    },
    {
      "name": "L1: Runtime"
    },
    {
      "name": "L1: Variables",
      "description": "Layer 1 REST mutations on global variables shared across graphs. Use these to coordinate state between graphs."
    },
    {
      "name": "L2 Animation: Actions",
      "description": "Layer 2 direct animation control. Play, stop, and transition between animation states."
    },
    {
      "name": "L2 Arrays: Actions",
      "description": "Layer 2 array operations. Build, index, slice, and reshape ordered collections."
    },
    {
      "name": "L2 Assets: Actions",
      "description": "Layer 2 bundled-asset operations. Spawn and configure assets at runtime."
    },
    {
      "name": "L2 Audio: Actions",
      "description": "Layer 2 direct audio control. Play, stop, and configure audio sources and listeners."
    },
    {
      "name": "L2 Camera: Actions",
      "description": "Layer 2 camera control. Activate, configure, and frame cameras for rendering."
    },
    {
      "name": "L2 Components: Actions",
      "description": "Layer 2 component operations. Add, remove, and configure components on scene objects."
    },
    {
      "name": "L2 Convert: Actions",
      "description": "Layer 2 type conversions. Cast and reshape values between graph data types."
    },
    {
      "name": "L2 Data: Actions",
      "description": "Layer 2 data-structure operations. Build, query, and transform structured data."
    },
    {
      "name": "L2 Datetime: Actions",
      "description": "Layer 2 date and time helpers. Format, parse, and compute time-based values."
    },
    {
      "name": "L2 Files: Actions",
      "description": "Layer 2 file I/O. Read, write, and inspect files on disk."
    },
    {
      "name": "L2 Generators: Actions",
      "description": "Layer 2 signal generators. Produce waves, noise, and other time-varying values."
    },
    {
      "name": "L2 Log: Actions",
      "description": "Layer 2 log emission. Write structured log lines to the OverMox console."
    },
    {
      "name": "L2 Midi: Actions",
      "description": "Layer 2 MIDI device control. Read and send MIDI messages from connected devices."
    },
    {
      "name": "L2 Output: Actions",
      "description": "Layer 2 output streams. Emit structured values to downstream consumers."
    },
    {
      "name": "L2 Particles: Actions",
      "description": "Layer 2 particle effect control. Emit, configure, and lifetime-manage particle systems."
    },
    {
      "name": "L2 Physics: Actions",
      "description": "Layer 2 direct actions on physics objects. Apply forces, set velocity, and configure physics state."
    },
    {
      "name": "L2 Process: Actions",
      "description": "Layer 2 process control. Launch and inspect external processes."
    },
    {
      "name": "L2 Project: Actions",
      "description": "Layer 2 project-level actions. Save, reload, and manage the active OverMox project."
    },
    {
      "name": "L2 Scene: Actions",
      "description": "Layer 2 direct actions on the scene. Spawn, destroy, set active, and inspect scene objects without modifying graphs."
    },
    {
      "name": "L2 Schema: Actions",
      "description": "Layer 2 schema introspection. Query and validate node and action schemas at runtime."
    },
    {
      "name": "L2 Shatter: Actions",
      "description": "Layer 2 shatter effects. Break scene objects into fragments at runtime."
    },
    {
      "name": "L2 Streamdeck: Actions",
      "description": "Layer 2 Elgato Stream Deck integration. Bind controls to graph triggers."
    },
    {
      "name": "L2 Streaming: Actions",
      "description": "Layer 2 streaming-event handlers. Trigger live-stream-driven graph reactions."
    },
    {
      "name": "L2 Text: Actions",
      "description": "Layer 2 text operations. Concatenate, format, and transform strings."
    },
    {
      "name": "L2 Transform: Actions",
      "description": "Layer 2 direct transform manipulation. Move, rotate, scale, and parent scene objects."
    },
    {
      "name": "L2 Tween: Actions",
      "description": "Layer 2 tween control. Animate property values over time with easing curves."
    },
    {
      "name": "L2 Variables: Actions",
      "description": "Layer 2 variable reads and writes. Direct access to graph and global variables outside the L1 REST surface."
    },
    {
      "name": "L2 Viewport: Actions",
      "description": "Layer 2 viewport control. Configure render targets and editor preview state."
    },
    {
      "name": "L2 Web: Actions",
      "description": "Layer 2 web requests. Issue HTTP GET, POST, and other calls to external services."
    },
    {
      "name": "L2 Webhook: Actions",
      "description": "Layer 2 webhook publishing. Send structured events to external HTTP listeners."
    },
    {
      "name": "L2 World: Actions",
      "description": "Layer 2 world-level actions. Manipulate global scene state and environment settings."
    },
    {
      "name": "L3: Events",
      "description": "Layer 3 server-pushed event stream over WebSocket. Subscribe to event types and receive each event as an envelope (type, timestamp, data) as it fires. See /asyncapi for the full event catalog."
    },
    {
      "name": "L2 Math: Actions",
      "description": "Layer 2 math operations. Pure-function arithmetic, vector, and curve helpers."
    }
  ],
  "servers": [
    {
      "url": "http://localhost:5000",
      "description": "Local OverMox API"
    }
  ]
}