{
  "openapi": "3.0.1",
  "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 (debug builds only)",
        "description": "Used by the OverMox sync pipeline to keep documentation images current.\r\nWhen the request body is null or NodeNames is empty, every node is rendered.\r\nOtherwise only the named nodes are rendered. Both the public and source\r\nasset directories receive the output (configurable via OVERMOX_PNG_OUT_DIRS).\r\nReturns 404 DEV_ENDPOINT_UNAVAILABLE when the host is not a debug build.",
        "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"
                }
              }
            }
          },
          "404": {
            "description": "The endpoint is unavailable because the host is not a debug build.",
            "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.",
        "operationId": "Discovery_GetSpawnables",
        "responses": {
          "200": {
            "description": "Spawnable Element list."
          },
          "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 graphs, nodes,\r\nconnections, ports, variables, layout, batch, and discovery operations.",
        "operationId": "Schema_GetLayer1",
        "responses": {
          "200": {
            "description": "Returns all Layer 1 operation schemas with field definitions."
          }
        }
      }
    },
    "/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, the canonical type label, the current value, the enum options array when applicable, a structural flag indicating whether the property reshapes the asset, and a per-property URL ready for direct GET or PUT.\r\nReturns 404 ASSET_NOT_FOUND if the asset tag is not recognized, 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"
                }
              }
            }
          },
          "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\nReturns 400 INVALID_REQUEST if the request body is missing or the properties array is empty, 404 ASSET_NOT_FOUND if the asset tag is not recognized, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. 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 so the scene re-skins atomically without partial visual states.*",
        "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": "Request body missing or properties array 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": "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"
                }
              }
            }
          },
          "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 names the resolved asset tag, the property name, the canonical type label, and the current value in its typed form.\r\nReturns 404 ASSET_NOT_FOUND if the asset is missing or 404 PROPERTY_NOT_FOUND if the property name does not exist on the asset, 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 material\u0027s albedo color and post the hex value 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 name, 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"
                }
              }
            }
          },
          "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 the body is missing 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, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected. 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 name, 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": "Invalid value for 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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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",
            "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; Trigger ports connect only to Trigger ports, and data ports connect to type-compatible data ports.\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 and 400 INCOMPATIBLE_TYPES if the port types cannot connect.\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",
            "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 (e.g., Trigger to data) or request body is 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 or one of the specified ports 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": "A connection already exists between these ports.",
            "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",
            "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",
            "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: true|false, reason: \u003Cstring|null\u003E} where reason is null when the pair is valid and a recovery hint otherwise.\r\nCall this before POST /api/graphs/{graphId}/connections to avoid 400 INCOMPATIBLE_TYPES round-trips.\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 null 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": "Request body is missing or incomplete.",
            "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, defaults to the project\u0027s Graphs folder. 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 an explicit path collides with another graph. 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, deletes any existing graph with the same name before creating the new one.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "sync",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "The request containing the 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": "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"
                }
              }
            }
          },
          "409": {
            "description": "No project is open and no explicit path was provided.",
            "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 both the in-project graph and the backing file on disk in a single transaction. Returns 400 UPDATE_FAILED when the new name is invalid or the filesystem rename fails, and 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",
            "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": "Rename failed (e.g., invalid name or filesystem error).",
            "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. 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",
            "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. 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",
            "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 and a derived name; the duplicate is added to the project but not saved to disk until POST /api/graphs/{id}/save. Returns 404 GRAPH_NOT_FOUND when no graph matches the supplied id and 409 GRAPH_RUNNING when the source graph is still executing. 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",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Graph was duplicated successfully.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphListItem"
                }
              }
            }
          },
          "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}/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",
            "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 current node and port state for the graph across SignalR so the OverMox runtime sees the latest topology before execution. 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": "Graph state was pushed to OverMox successfully."
          },
          "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 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"
                }
              }
            }
          },
          "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 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"
                }
              }
            }
          },
          "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",
            "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/OpenProjectRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            }
          }
        },
        "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 (dropdowns first so dependent ports exist), then wires connections between ports. On failure at any phase, every connection and node created earlier in the call is rolled back. 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. Limits: 1000 nodes and 5000 connections per call. Returns 400 INVALID_REQUEST, BATCH_TOO_LARGE, or DUPLICATE_TEMP_ID for shape violations and 404 GRAPH_NOT_FOUND when the graph id does not exist. 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, sync changes to OverMox after the build completes.",
            "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": "Build failed at some phase. Returns phase, step, error code, and rollback status.",
            "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",
            "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",
            "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",
            "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",
            "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",
            "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",
            "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",
            "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",
            "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, sync graph state to OverMox after the update. Note formatting is local-only,\r\n             so this flag has no practical effect, but is accepted for surface consistency with other L1 mutations.",
            "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 was empty, the node is not a Note, or a value is not in the allowed list.",
            "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 ACTION_FAILED if the graph is not currently running, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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.",
            "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/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}/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, 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"
                }
              }
            }
          },
          "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 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, 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"
                }
              }
            }
          },
          "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, 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 name (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"
                }
              }
            }
          },
          "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, Color accepts an sRGB hex or {r, g, b, a} object, Vector3 accepts {x, y, z}. Read GET /api/objects/{tag}/components/{component}/{property} first to discover the expected type and any enum options.\r\nReturns 400 INVALID_REQUEST when the body is missing or 400 INVALID_VALUE when the value cannot be coerced to the property\u0027s type, 404 OBJECT_NOT_FOUND, COMPONENT_NOT_FOUND, or PROPERTY_NOT_FOUND when the target does not resolve, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\r\nSee PUT /api/objects/{tag}/components/{component}/properties for atomic multi-property writes.\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 name (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": "Invalid value for 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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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; a single property failure populates that record\u0027s error field without stopping the rest. The results array preserves request order so callers can correlate writes to outcomes.\r\nReturns 400 INVALID_REQUEST when the request body or properties array is missing or empty, 404 OBJECT_NOT_FOUND or COMPONENT_NOT_FOUND when the tag or component does not resolve, and 503 OVERMOX_NOT_CONNECTED when no OverMox runtime is connected.\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 a single bulk write that sets logo color, light intensity, and background tint together so the on-stream switch lands atomically rather than one knob at a time.*",
        "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": "Request body missing or properties array 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 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"
                }
              }
            }
          },
          "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 SET_VALUE_FAILED if the payload cannot coerce to the port\u0027s type, 400 INVALID_VALUE if a dropdown receives an unknown option, and 404 PORT_NOT_FOUND if the port ID does not resolve.\r\nValue format depends on port type: Float=number, Integer=integer, String=\u0022text\u0022, Boolean=true/false, Vector={x,y,z} or [x,y,z] or scalar uniform, Color={r,g,b,a} or [r,g,b,a], Dropdown/Enum=option name or index. Trigger and Button ports are write-protected. See GET /api/types for the canonical value-format table.\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": "When true, fires a SyncGraphs to OverMox after the value lands.",
            "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": "Value could not be converted to the port\u0027s type, or the port is read-only.",
            "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": "When true, fires a SyncGraphs to OverMox after the expansion lands.",
            "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": "When true, fires a SyncGraphs to OverMox after the collapse lands.",
            "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, sync changes to OverMox after the toggle.",
            "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, sync changes to OverMox after the toggle.",
            "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": "Returns 200 with the options list on success. Returns 400 INVALID_REQUEST when the port is not a Dropdown or Enum type, and 404 NOT_FOUND if the 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."
          },
          "400": {
            "description": "Port is not a dropdown 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": "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\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"
                }
              }
            }
          },
          "422": {
            "description": "OverMox processed the command but it 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.\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."
          }
        }
      }
    },
    "/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",
            "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",
            "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",
            "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} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or the requested clip name is unknown, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 key \u0022object\u0022 (String). Optional keys \u0022animation\u0022 (String, defaults to the Animator\u0027s current state when omitted), \u0022crossFadeTime\u0022 (Float), and \u0022speed\u0022 (Float).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animation started."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component or unknown clip.",
            "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 an animation",
        "description": "Returns 200 with {ok: true, action: \u0022animation.stop\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or no animation is currently playing, 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).",
          "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\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/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 key \u0022animation\u0022 (String, defaults to the Animator\u0027s 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": "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} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no Animator or the destination state is unknown, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/animation/play for an immediate state swap without blending.\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 key \u0022time\u0022 (Float, blend duration in seconds).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transition started."
          },
          "422": {
            "description": "Action failed - e.g., no Animator component or unknown destination state.",
            "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} on success.\r\nReturns 422 ACTION_FAILED if the named scene object is missing the supporting physics components, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nHigher power values produce a larger initial displacement; 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."
          },
          "422": {
            "description": "Action failed - e.g., missing physics 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/arrays/create": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Create an array variable",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.create\u0022} when the variable is created with the requested type and initial length.\r\nReturns 422 ACTION_FAILED if the name is already in use or the type is not recognized, 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, Color), \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."
          },
          "422": {
            "description": "Action failed - name already in use or type not recognized.",
            "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 length matches the requested size; existing entries are preserved up to the new length and additional slots use the type default.\r\nReturns 422 ACTION_FAILED if the named array does not exist or the size is negative, 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."
          },
          "422": {
            "description": "Action failed - array missing or size 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/arrays/add": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Append to an array",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.add\u0022} when the value is appended at the new tail index; the array length increases by one.\r\nReturns 422 ACTION_FAILED if the named array does not exist 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 appended successfully."
          },
          "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/add-row": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Append a row",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.addRow\u0022} when the row is appended at the new bottom index; the array row count increases by one.\r\nReturns 422 ACTION_FAILED if the named array is not two-dimensional, the value list is missing, or any cell is not coercible, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/arrays/remove-row for the inverse and POST /api/actions/arrays/add for the 1D variant.\r\n            \r\n*Example: When a viewer-built leaderboard segment records the next round of scores, append a row containing the supporter name and three category totals 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": "Row appended successfully."
          },
          "422": {
            "description": "Action failed - array missing or row 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 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 422 ACTION_FAILED if the index is out of range 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/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."
          },
          "422": {
            "description": "Action failed - 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.\r\nReturns 422 ACTION_FAILED if the index is out of range or the array does not exist, 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."
          },
          "422": {
            "description": "Action failed - index out of range or 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/size": {
      "post": {
        "tags": [
          "L2 Arrays: Actions"
        ],
        "summary": "Read array length",
        "description": "Returns 200 with {ok: true, action: \u0022arrays.size\u0022, size: \u003CInteger\u003E, sizeX: \u003CInteger\u003E, sizeY: \u003CInteger\u003E} where size is the entry count of the 1D array or the row count of a 2D array, and sizeX and sizeY are the 2D column and row counts.\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 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."
          },
          "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/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 422 ACTION_FAILED if the named array does not exist 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/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."
          },
          "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 {ok: true, action: \u0022arrays.indexOf\u0022, indexX: \u003CInteger\u003E, indexY: \u003CInteger\u003E} where indexX is the earliest column position and indexY is the row position whose entry equals the supplied value, or both -1 when no entry matches.\r\nReturns 422 ACTION_FAILED if the named array does not exist 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/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."
          },
          "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/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 {ok: true, action: \u0022assets.list\u0022, assets: [...]} naming runtime assets and Bundled Assets together.\r\nReturns 422 ACTION_FAILED if the filter parameters are malformed, 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."
          },
          "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/create": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Create a project asset",
        "description": "Returns 200 with {ok: true, action: \u0022assets.create\u0022, tag: \u003Cnew\u003E} when the asset type is recognized and creation succeeds.\r\nReturns 422 ACTION_FAILED if the asset type is unknown or the seed data is rejected, 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 project subfolder path).",
          "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."
          },
          "422": {
            "description": "Action failed - invalid asset 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/delete": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Delete a project asset",
        "description": "Returns 200 with {ok: true, action: \u0022assets.delete\u0022, deleted: true} when the asset is removed. Bundled Assets remove the underlying .ovprefab file from disk and cannot be undone.\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 {ok: true, action: \u0022assets.addData\u0022, added: true} when the payload validates and is appended or merged into the target asset.\r\nReturns 422 ACTION_FAILED if the asset is not found or the data shape is rejected, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/assets/create to allocate an asset first and POST /api/actions/assets/editFilter to tune how the data is interpreted.\r\n            \r\n*Example: When a chat command requests a custom alert image, add the pixel payload returned from an HTTP fetch into a freshly created Image asset so the alert overlay can display it.*",
        "operationId": "Actions_AssetsAddData",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String) and \u0022data\u0022 (object, an asset-type-specific payload).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data added to asset."
          },
          "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/editFilter": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Edit an asset filter",
        "description": "Returns 200 with {ok: true, action: \u0022assets.editFilter\u0022} when the named filter is applied or its settings updated.\r\nReturns 422 ACTION_FAILED if the asset is not found or the filter name is unknown, 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, edit the asset filter on the on-stream camera feed to a CRT-style settings preset so the segment shifts visually.*",
        "operationId": "Actions_AssetsEditFilter",
        "requestBody": {
          "description": "Required dictionary with keys \u0022asset\u0022 (String, an asset tag), \u0022property\u0022 (String, the property name on the filter), and \u0022value\u0022 (Object, the new property value). Optional key \u0022filter\u0022 (Integer, the zero-based filter index; defaults to 0).",
          "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."
          },
          "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/createBundled": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Create a Bundled Asset",
        "description": "Returns 200 with {ok: true, action: \u0022assets.createBundled\u0022, tag: \u003Csource\u003E, name: \u003Casset\u003E, itemCount: \u003Cn\u003E} when the scene object is serialized to an .ovprefab on disk.\r\nReturns 422 ACTION_FAILED if the scene object is not found or the destination folder is invalid, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 project subfolder path).",
          "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."
          },
          "422": {
            "description": "Action failed - 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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022assets.getProperty\u0022, tag: \u003Casset\u003E, property: \u003Cname\u003E, type: \u003Ccanonical\u003E, value: \u003Ccurrent\u003E} when the asset and property both resolve.\r\nReturns 422 ACTION_FAILED if the asset or property is missing, 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 logo\u0022, read the tint property on the logo Image 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."
          },
          "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/setProperty": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Write an asset property",
        "description": "Returns 200 with {ok: true, action: \u0022assets.setProperty\u0022, tag: \u003Casset\u003E, property: \u003Cname\u003E, type: \u003Ccanonical\u003E, previousValue: \u003Cprior\u003E, value: \u003Ccoerced\u003E} when the asset and property resolve and the value coerces.\r\nReturns 422 ACTION_FAILED if the asset or property is missing 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 channel-point reward changes the on-stream backdrop tint, write the chosen color to the tint property of the backdrop Image asset so the change is visible on the next frame.*",
        "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."
          },
          "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/setProperties": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Bulk-update asset properties",
        "description": "Returns 200 with {ok: true, action: \u0022assets.setProperties\u0022, tag: \u003Casset\u003E, results: [...]} where each result names the property, the previous value, and either the new value or an error reason for that property.\r\nReturns 422 ACTION_FAILED if the asset is missing or the request shape is invalid, 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 tint, scale, and rotation properties on the redeemed asset in one atomic call so the visual change lands as a single beat instead of three.*",
        "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 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/getShader": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Read material shader",
        "description": "Returns 200 with {ok: true, action: \u0022assets.getShader\u0022, asset: \u003Ctag\u003E, shader: \u003Cname\u003E, assetType: \u003Ckind\u003E} naming the active shader and the host asset kind.\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 {ok: true, action: \u0022assets.changeShader\u0022, asset: \u003Ctag\u003E, previousShader: \u003Cprior\u003E, shader: \u003Cnew\u003E} when the shader name resolves and is applied.\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 {ok: true, action: \u0022assets.listShaders\u0022, shaders: [...], count: N} naming every shader compatible with the optional type filter.\r\nReturns 422 ACTION_FAILED if the type filter is malformed, 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."
          },
          "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/getProperties": {
      "post": {
        "tags": [
          "L2 Assets: Actions"
        ],
        "summary": "Bulk-read asset properties",
        "description": "Returns 200 with {ok: true, action: \u0022assets.getProperties\u0022, asset: \u003Ctag\u003E, results: [{name, type, value}, ...], errors: [{name, error}, ...]} so callers can render mixed success and failure outcomes in one pass.\r\nReturns 422 ACTION_FAILED if the asset is missing or the request shape is invalid, 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 asset, read tint, scale, and shader properties 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 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/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 {ok: true, action: \u0022audio.decibel\u0022, value: \u003Cfloat\u003E} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no AudioVisualizer or the channel index is out of range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe reading reflects the current frame; poll on a Tick node to drive continuous overlays.\r\n            \r\n*Example: When the streamer\u0027s microphone level crosses a loudness threshold, sample this each tick and feed the value into a Lerp 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."
          },
          "422": {
            "description": "Action failed - e.g., source cannot be sampled.",
            "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 422 ACTION_FAILED 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, named Camera transition or blend effect to apply during the switch).",
          "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."
          },
          "422": {
            "description": "Action failed because 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 {ok: true, action: \u0022components.reset\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\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 {ok: true, action: \u0022components.refresh\u0022, refreshed: [\u003Ccomponent type names\u003E]} on success.\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 {ok: true, action: \u0022convert.type\u0022, value: \u003Cconverted\u003E} on success.\r\nReturns 422 ACTION_FAILED if the source and target types are not coercible, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nTarget type names match the canonical labels from GET /api/types (Boolean, Float, Integer, String, Vector3, Color, etc.).\r\n            \r\n*Example: When a viewer types a hex color in chat, convert the String into a Color and feed it to a light node that recolors 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."
          },
          "422": {
            "description": "Action failed - e.g., incompatible 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": "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 {ok: true, action: \u0022data.primitiveValue\u0022, value: \u003Ctyped payload\u003E} on success.\r\nReturns 422 ACTION_FAILED if the value cannot be parsed into the requested type, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 sets a target sub goal number, wrap the parsed Integer with this endpoint and feed it into a counter node that drives an on-stream progress bar.*",
        "operationId": "Actions_DataPrimitiveValue",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022type\u0022 (String, one of int, integer, float, bool, boolean, string, Vector3, Color) 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.",
            "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 {ok: true, action: \u0022datetime.get\u0022, value: \u003Cformatted string\u003E} on success.\r\nReturns 422 ACTION_FAILED if the format pattern is invalid, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nDefault format is the .NET round-trip ISO 8601 representation when no format is supplied.\r\n            \r\n*Example: When a viewer asks the streamer\u0027s bot for \u0022show start time\u0022, read the current UTC timestamp and feed it into a Text 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 {ok: true, action: \u0022files.read\u0022, content: \u003Ctext\u003E} on success.\r\nReturns 422 ACTION_FAILED if 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 Text 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."
          },
          "422": {
            "description": "Action failed - e.g., 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 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": "Required dictionary with keys \u0022path\u0022 (String, full file path), \u0022folder\u0022 (String, project-relative subfolder used when path is omitted), and \u0022file_name\u0022 (String, file name within folder). Optional keys \u0022content\u0022 (String), \u0022mode\u0022 (String: \u0022text\u0022, \u0022bytes\u0022, or \u0022binary\u0022), and \u0022data\u0022 (String, base64-encoded binary alias for content).",
          "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., 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 {ok: true, action: \u0022generators.random\u0022, value: \u003Cgenerated\u003E} on success.\r\nReturns 422 ACTION_FAILED if the requested type is unsupported, 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, \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.",
            "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 {ok: true, action: \u0022generators.wave\u0022, value: \u003Csampled\u003E} on success.\r\nReturns 422 ACTION_FAILED if the requested waveform is unsupported, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/generators/random for one-shot randomization and POST /api/actions/transform/setPosition for binding 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.",
            "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 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/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."
          },
          "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/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 if no MIDI output device is connected or the channel and command pair is invalid, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nWhen \u0022device\u0022 is omitted the message is sent to the default output. 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 \u0022channel\u0022 (Integer, MIDI channel 1-16), \u0022event_type\u0022 (String), \u0022number\u0022 (Integer), \u0022note\u0022 (Integer), \u0022velocity\u0022 (Integer), \u0022value\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 - e.g., 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 {ok: true, action: \u0022midi.subscribe\u0022, subscriptionId: \u003Cstring\u003E} on success.\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 {ok: true, action: \u0022midi.unsubscribe\u0022} on success.\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 {ok: true, action: \u0022midi.devices\u0022, inputs: [\u003Cstring\u003E], outputs: [\u003Cstring\u003E]} on success.\r\nReturns 422 ACTION_FAILED if the host MIDI subsystem cannot enumerate devices, and 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 - e.g., device enumeration error.",
            "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 {ok: true, action: \u0022output.get\u0022, value: \u003Csettings\u003E} on success, where settings names resolutionX, resolutionY, frameRate, vSyncEnabled, spoutEnabled, ndiEnabled, virtualWebcamEnabled, showMarkup, and transparentBGEnabled.\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 numeric fields and POST /api/actions/output/toggle to flip boolean fields.\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 {ok: true, action: \u0022output.set\u0022, value: \u003Csettings\u003E} on success, where settings names the full output settings record after the change.\r\nReturns 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), \u0022virtualWebcamEnabled\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"
                }
              }
            }
          }
        }
      }
    },
    "/api/actions/output/toggle": {
      "post": {
        "tags": [
          "L2 Output: Actions"
        ],
        "summary": "Toggle a boolean output setting",
        "description": "Returns 200 with {ok: true, action: \u0022output.toggle\u0022, value: \u003Csettings\u003E} on success, where settings names the full output settings record after the flip.\r\nReturns 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, toggle spout on for thirty seconds to push a celebratory feed into a downstream compositor, then toggle it back off from a delayed trigger node.*",
        "operationId": "Actions_OutputToggle",
        "requestBody": {
          "description": "Required dictionary with key \u0022setting\u0022 (String, one of \u0022vsync\u0022, \u0022spout\u0022, \u0022ndi\u0022, \u0022virtualWebcam\u0022, \u0022showMarkup\u0022, \u0022transparentBG\u0022). Optional key \u0022enabled\u0022 (Boolean); when omitted 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"
                }
              }
            }
          }
        }
      }
    },
    "/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\nReturns 422 ACTION_FAILED if the named scene object has no Rigidbody, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe force vector is interpreted in world space; 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 \u0022AddForce\u0022, \u0022AddRelativeForce\u0022, \u0022Torque\u0022, \u0022AddForceAtPosition\u0022), \u0022force_mode\u0022 (String, one of \u0022Force\u0022, \u0022Impulse\u0022, \u0022VelocityChange\u0022, \u0022Acceleration\u0022), and \u0022position\u0022 (Vector3, used when force_type is \u0022AddForceAtPosition\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."
          },
          "422": {
            "description": "Action failed - e.g., object has no Rigidbody.",
            "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\nReturns 422 ACTION_FAILED if the explosion cannot be applied, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe force value scales linearly with proximity to the origin; upwards_mod lifts the apparent origin downward to bias trajectories upward and defaults to zero when omitted. See POST /api/actions/physics/addForce for single-target pushes.\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 so every loose Rigidbody 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 the scene object\u0027s position), \u0022force\u0022 (Float, defaults to a moderate impulse magnitude), \u0022radius\u0022 (Float, defaults to the runtime\u0027s standard explosion radius), \u0022upwards_mod\u0022 (Float), and \u0022force_mode\u0022 (String, one of \u0022Force\u0022, \u0022Impulse\u0022, \u0022VelocityChange\u0022, \u0022Acceleration\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."
          },
          "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\nReturns 422 ACTION_FAILED if the named scene object has no Rigidbody, 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."
          },
          "422": {
            "description": "Action failed - e.g., no Rigidbody 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/physics/getVelocity": {
      "post": {
        "tags": [
          "L2 Physics: Actions"
        ],
        "summary": "Read Rigidbody velocity",
        "description": "Returns 200 with {ok: true, action: \u0022physics.getVelocity\u0022, velocity: {x, y, z}} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no Rigidbody, 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.",
            "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\nReturns 422 ACTION_FAILED if the named scene object has no Rigidbody, 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 the current Rigidbody position).",
          "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."
          },
          "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\nReturns 422 ACTION_FAILED if the named scene object has no Rigidbody, 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 the current Rigidbody 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."
          },
          "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 {ok: true, action: \u0022process.start\u0022, processId: \u003Cint\u003E, processName: \u003Cstring\u003E} on success.\r\nReturns 422 ACTION_FAILED if 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."
          },
          "422": {
            "description": "Action failed - e.g., 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.\r\nReturns 422 ACTION_FAILED if the process cannot be found or the host denies access, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 stalled audio host process so the streamer can restart it without restarting the whole stream.*",
        "operationId": "Actions_ProcessTerminate",
        "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": "Process terminated."
          },
          "422": {
            "description": "Action failed - e.g., process 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/process/wait": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Wait for a process to exit",
        "description": "Returns 200 with {ok: true, action: \u0022process.wait\u0022, hasExited: \u003Cbool\u003E, exitCode: \u003Cint\u003E} once the process exits within the timeout window.\r\nReturns 422 ACTION_FAILED if the process is unknown or the timeout elapses before exit, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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). 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."
          },
          "422": {
            "description": "Action failed - e.g., timeout exceeded.",
            "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/status": {
      "post": {
        "tags": [
          "L2 Process: Actions"
        ],
        "summary": "Read process status",
        "description": "Returns 200 with {ok: true, action: \u0022process.status\u0022, hasExited: \u003Cbool\u003E, exitCode: \u003Cint\u003E, processName: \u003Cstring\u003E, processId: \u003Cint\u003E}.\r\nReturns 422 ACTION_FAILED if the process ID is unknown, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nPoll this on a Tick node to drive an on-stream readout; pair with POST /api/actions/process/start to capture the ID and POST /api/actions/process/terminate to act on the result.\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 the running flag into a Text 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."
          },
          "422": {
            "description": "Action failed - e.g., process 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/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 {ok: true, action: \u0022project.refresh\u0022} on success.\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 {ok: true, action: \u0022scene.switch\u0022} on success.\r\nReturns 422 ACTION_FAILED if the scene name or build index does not resolve, 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, scene name or build index).",
          "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 the scene name or build index 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/reset": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Reset the scene",
        "description": "Returns 200 with {ok: true, action: \u0022scene.reset\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/scene/clear for removing all scene objects without restoring spawn defaults.\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 required.",
          "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 the named scene 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/spawn": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Spawn a scene object",
        "description": "Returns 200 with {tag, name, active} describing the spawned object on success.\r\nReturns 422 ACTION_FAILED if the asset or primitive type does not resolve, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nPrimitives supported are Cube, Sphere, Capsule, Cylinder, Plane, Quad; lights supported are DirectionalLight, PointLight, SpotLight, AreaLight. Tags are auto-deduplicated when they collide. 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, primitive type like Cube or light type like DirectionalLight), \u0022sceneObject\u0022 (String, existing object tag to duplicate), \u0022asset\u0022 (String, Bundled Asset 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 the asset or primitive type 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/clear": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Clear all scene objects",
        "description": "Returns 200 with {ok: true, action: \u0022scene.clear\u0022, cleared: \u003Ccount\u003E} on success, where cleared is the number of scene objects removed.\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 restoring spawn-time objects rather than clearing everything.\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 (string[], scene object tags to keep).",
          "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 {ok: true, action: \u0022scene.objects.find\u0022, objects: [\u003Ctag\u003E, ...]} on success; objects is empty when nothing matches.\r\nReturns 422 ACTION_FAILED if 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 trophy\u0022, find scene objects tagged \u0022trophy\u0022 so a follow-up setColor or setActive call can light just those objects.*",
        "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 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 {ok: true, action: \u0022scene.objects.findWithComponent\u0022, objects: [\u003Ctag\u003E, ...]} on success; objects is empty when no scene object carries the requested component.\r\nReturns 422 ACTION_FAILED if the component type name does not resolve, and 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 type 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 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 {ok: true, action: \u0022scene.objects.getChild\u0022, child: \u003Ctag\u003E} on success.\r\nReturns 422 ACTION_FAILED if the parent does not resolve or the index is out of range, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 or index 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 {ok: true, action: \u0022scene.objects.getChildCount\u0022, count: \u003CInteger\u003E} on success.\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 {ok: true, action: \u0022scene.objects.getParent\u0022, parent: \u003Ctag-or-null\u003E} on success.\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/setTag for the engine-tag property.\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": "Set engine tag property",
        "description": "Returns 200 with {tag, name, active} reflecting the updated engine tag on success; the ItemTag identifier is preserved.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve or the engine tag value is rejected, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe engine tag drives runtime tag-based lookups separate from the ItemTag. See POST /api/actions/scene/objects/setName for the display name.\r\n            \r\n*Example: When the streamer reorganizes overlays mid-show and wants every \u0022interactive\u0022 prop to respond to a single tag-based query, set the engine tag on each affected scene object so the next find-by-tag call sweeps them all in one pass.*",
        "operationId": "Actions_SceneObjectsSetTag",
        "requestBody": {
          "description": "Required dictionary with keys \u0022object\u0022 (String, the persistent ItemTag) and \u0022tag\u0022 (String, the engine tag value to assign).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Engine tag set successfully."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve or the engine tag 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/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 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 or 9-31; 6/7/8 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 {ok: true, action: \u0022scene.objects.convert\u0022} on success.\r\nReturns 422 ACTION_FAILED if the ItemTag does not resolve or the target type label is rejected, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe conversion preserves Transform values where possible. See POST /api/actions/scene/spawn for creating a fresh object of the target type instead.\r\n            \r\n*Example: When a viewer-built object needs to switch from a static prop to a rigged Animator-driven asset for a \u0022make it dance\u0022 reward, convert the scene object in place so connected ports and overlays retain their references.*",
        "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 or target type 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/setColor": {
      "post": {
        "tags": [
          "L2 Scene: Actions"
        ],
        "summary": "Set object color",
        "description": "Returns 200 with {ok: true, action: \u0022scene.setColor\u0022, tag: \u003Ctag\u003E, r, g, b, a} on success.\r\nReturns 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 changing object type while preserving color.\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 key \u0022object\u0022 (String, target ItemTag); optional key \u0022color\u0022 (Color, supplied as {r,g,b,a} object or [r,g,b,a] array with values in 0-1).",
          "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."
          },
          "422": {
            "description": "Action failed because the ItemTag did not resolve 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 422 ACTION_FAILED 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, named Camera transition or blend effect to apply during the switch).",
          "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."
          },
          "422": {
            "description": "Action failed because 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 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\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",
        "responses": {
          "200": {
            "description": "Returns all action schemas.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSchemaCatalogResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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 {ok: true, action: \u0022shatter.init\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no mesh, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\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 mesh so the reward redemption later in the show triggers an instant fragmentation.*",
        "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."
          },
          "422": {
            "description": "Action failed - e.g., object has no mesh.",
            "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 {ok: true, action: \u0022shatter.activate\u0022} on success.\r\nReturns 422 ACTION_FAILED if the object was never initialized via POST /api/actions/shatter/init, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/shatter/fade to dissolve the resulting fragments and POST /api/actions/shatter/demo to chain init, activate, and fade in one call.\r\n            \r\n*Example: When a donation crosses the \u0022smash the vase\u0022 tier on screen, activate the pre-initialized vase with a strong force value and a downward direction Vector3 so the on-stream prop bursts apart toward the floor in time with the alert.*",
        "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."
          },
          "422": {
            "description": "Action failed - e.g., not initialized.",
            "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": "Fade shatter fragments",
        "description": "Returns 200 with {ok: true, action: \u0022shatter.fade\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no active shatter fragments, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nFragments fade over the system\u0027s built-in fade time. See POST /api/actions/shatter/activate for the breakup step that produces the fragments this endpoint then dissolves.\r\n            \r\n*Example: When a celebration alert finishes playing after a \u0022blow it up\u0022 subscriber reward, fade the leftover shards over two seconds so the on-stream debris drifts out cleanly 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."
          },
          "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/shatter/demo": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Run a shatter demo",
        "description": "Returns 200 with {ok: true, action: \u0022shatter.demo\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no mesh, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe endpoint chains init, activate, and fade with sensible defaults so a single call is enough to play a full destruction beat. See POST /api/actions/shatter/init, POST /api/actions/shatter/activate, and POST /api/actions/shatter/fade for fine-grained control across each phase.\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 without staging the steps individually.*",
        "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": "Demo started."
          },
          "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/shatter/fragment": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Configure shatter fragmentation",
        "description": "Returns 200 with {ok: true, action: \u0022shatter.fragment\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object was never initialized via POST /api/actions/shatter/init, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/shatter/scale to resize the resulting pieces.\r\n            \r\n*Example: When a channel point reward lets a viewer pick \u0022shatter style\u0022 before a destruction beat, configure the prepared prop with their requested piece count and pattern so the next on-stream break-up matches the reward they chose.*",
        "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": "Fragmentation configured."
          },
          "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/shatter/scale": {
      "post": {
        "tags": [
          "L2 Shatter: Actions"
        ],
        "summary": "Scale shatter fragments",
        "description": "Returns 200 with {ok: true, action: \u0022shatter.scale\u0022} on success.\r\nReturns 422 ACTION_FAILED if the named scene object has no active shatter fragments, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nA Float scale value multiplies every axis uniformly while a Vector3 scale value applies per-axis multipliers; the scale is applied to each fragment after activation. See POST /api/actions/shatter/fragment to change the piece count and POST /api/actions/shatter/fade to dissolve the resized fragments.\r\n            \r\n*Example: When a moderator says \u0022make those shards big\u0022 during a destruction segment, scale the active fragments to twice their original size 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 or per-axis scaling).",
          "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."
          },
          "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/streamdeck/update-button": {
      "post": {
        "tags": [
          "L2 Streamdeck: Actions"
        ],
        "summary": "Update Stream Deck button",
        "description": "Sends the update to the connected Elgato Stream Deck device so the button reflects the new visual state.\r\nReturns 200 with {ok: true, action: \u0022streamdeck.updateButton\u0022} on success.\r\nReturns 422 ACTION_FAILED if the button index is out of range or the Stream Deck rejects the update, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/streamdeck/* for the full Stream Deck action surface.\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": "Button state updated successfully."
          },
          "422": {
            "description": "Action failed on the OverMox side; for example, an invalid button index or a Stream Deck reject.",
            "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 {ok: true, action: \u0022streaming.platforms\u0022, platforms: [...]} on success.\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.\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 {ok: true, action: \u0022streaming.subscribe\u0022, subscriptionId: \u003Cstring\u003E} on success.\r\nReturns 422 ACTION_FAILED if the platform is not connected or the eventType is unrecognized, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSubscribed events are broadcast as \u0022streaming.event\u0022 L3 messages on the WebSocket hub, rate-limited to 100 events per second. See POST /api/actions/streaming/platforms for valid platform and eventType pairs, and POST /api/actions/streaming/unsubscribe to stop delivery.\r\n            \r\n*Example: When a streamer enables a \u0022donations drive overlays\u0022 segment, subscribe to the platform\u0027s donation event so each incoming tip flows through a graph that updates an on-stream goal bar.*",
        "operationId": "Actions_StreamingSubscribe",
        "requestBody": {
          "description": "Required dictionary with keys \u0022platform\u0022 (String) and \u0022eventType\u0022 (String). Optional key \u0022token\u0022 (String) required for StreamElements and StreamLabs.",
          "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., platform not connected or invalid event 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/streaming/unsubscribe": {
      "post": {
        "tags": [
          "L2 Streaming: Actions"
        ],
        "summary": "Cancel a platform subscription",
        "description": "Returns 200 with {ok: true, action: \u0022streaming.unsubscribe\u0022} on success.\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 {ok: true, action: \u0022streaming.status\u0022, subscriptions: [...]} on success.\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\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 {ok: true, action: \u0022text.format\u0022, result: \u003Cstring\u003E} on success.\r\nPlaceholders in the template use {name} syntax and are filled from the values dictionary; missing keys leave the placeholder as a literal.\r\nReturns 422 ACTION_FAILED if the template is malformed, 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 Text variable driving an on-stream caption.*",
        "operationId": "Actions_TextFormat",
        "requestBody": {
          "description": "Required dictionary with key \u0022template\u0022 (String). Placeholder values are sourced from the surrounding graph variables.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "String formatted."
          },
          "422": {
            "description": "Action failed - e.g., malformed template.",
            "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 {ok: true, action: \u0022text.parse\u0022, result: \u003Cvalue\u003E} on success.\r\nThe shape of result depends on the type parameter: split returns an Array of String, regex returns the matched group(s), json returns the value at the property path, xml returns the inner XML of the element at the property path.\r\nReturns 422 ACTION_FAILED if the pattern is an invalid regex or the input is not valid JSON or XML, 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 types \u0022!cue intro,music,lights\u0022, parse the message in split mode and feed the resulting Array into a sequencer node that fires three scene cues in order.*",
        "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."
          },
          "422": {
            "description": "Action failed - e.g., invalid regex pattern.",
            "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 {ok: true, action: \u0022text.operation\u0022, result: \u003Cvalue\u003E} on success.\r\nThe result type follows the operation: Length and IndexOf return Integer, Contains returns Boolean, Split returns two strings (result and result2), and the other operations return String.\r\nReturns 422 ACTION_FAILED if the operation name is unknown or required keys for that operation are missing, 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 Text variable driving a flashing on-stream banner.*",
        "operationId": "Actions_TextOperation",
        "requestBody": {
          "description": "Required dictionary with key \u0022input\u0022 (String). Optional keys \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; defaults to \u0022Append\u0022 when omitted), \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."
          },
          "422": {
            "description": "Action failed - e.g., unknown operation or missing operand.",
            "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 {ok: true, action: \u0022transform.get\u0022, position: Vector3, rotation: Vector3 Euler degrees, scale: Vector3} on success.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, 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."
          },
          "422": {
            "description": "Scene object not found 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 422 ACTION_FAILED if the named scene object cannot be found 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."
          },
          "422": {
            "description": "Scene object not found 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 422 ACTION_FAILED if the named scene object cannot be found, 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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 422 ACTION_FAILED if the named scene object cannot be found 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."
          },
          "422": {
            "description": "Scene object not found 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 (use the tween endpoints for animated turns).\r\nReturns 422 ACTION_FAILED if the source or named target scene object cannot be found, 400 INVALID_REQUEST if neither \u0022target\u0022 nor \u0022position\u0022 is supplied, 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": "Neither target nor position 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": "Source or target 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"
                }
              }
            }
          },
          "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 422 ACTION_FAILED if the named scene object cannot be found 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."
          },
          "422": {
            "description": "Scene object not found 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 {ok: true, action: \u0022transform.smoothMove\u0022, tag, name, active} as soon as the move begins (plus {tweenId, duration} when an animation is queued); the runtime continues damping toward the target over subsequent frames until the scene object converges.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/tween/position for fixed-duration eased motion 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) and \u0022smoothTime\u0022 (Float, seconds).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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\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 {ok: true, action: \u0022world.toViewport\u0022, x: Float, y: Float, z: Float} on success, where x and y are normalized viewport coordinates and z is the depth in world units from the camera.\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\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.",
            "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 {ok: true, action: \u0022tween.position\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime drives the position over the requested duration applying the named easing curve.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/smoothMove for damped (non-fixed-duration) motion 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022tween.rotation\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime interpolates rotation via the shortest Quaternion arc and applies the named easing curve over the requested duration.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/lookAt for snap aiming 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022tween.scale\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime drives local scale over the requested duration applying the named easing curve.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, 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 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022tween.position\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime drives the position over the requested duration applying the named easing curve.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/smoothMove for damped (non-fixed-duration) motion 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022tween.rotation\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime interpolates rotation via the shortest Quaternion arc and applies the named easing curve over the requested duration.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nSee POST /api/actions/transform/lookAt for snap aiming 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022tween.scale\u0022, tag, tweenId, duration} as soon as the tween starts; the runtime drives local scale over the requested duration applying the named easing curve.\r\nReturns 422 ACTION_FAILED if the named scene object cannot be found, 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 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), and \u0022ease\u0022 (String, easing curve name).",
          "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."
          },
          "422": {
            "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"
                }
              }
            }
          },
          "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 {ok: true, action: \u0022variables.create\u0022} on success.\r\nReturns 400 INVALID_REQUEST when \u0022type\u0022 is missing 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, 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 key \u0022name\u0022 (String). Optional keys \u0022type\u0022 (String, one of \u0022Boolean\u0022, \u0022Float\u0022, \u0022Integer\u0022, \u0022String\u0022, \u0022Vector3\u0022; defaults to \u0022String\u0022 when omitted) and \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 or invalid \u0022type\u0022 parameter.",
            "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 {ok: true, action: \u0022variables.get\u0022, value: \u003Ctyped\u003E} on success.\r\nThe value field is shaped to the variable\u0027s canonical type: Boolean, Float, Integer, String, or Vector3.\r\nReturns 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 node that posts a \u0022we are at $X\u0022 reply.*",
        "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."
          },
          "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 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."
          },
          "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\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/web/http-request": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send an HTTP request",
        "description": "Returns 200 with {ok: true, action: \u0022http.request\u0022, statusCode: \u003Cint\u003E, headers: \u003Cobject\u003E, body: \u003Ctext\u003E, bytes: \u003Cbase64\u003E} on success.\r\nReturns 422 ACTION_FAILED on network error, DNS failure, or timeout, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nDefault method is GET when \u0022method\u0022 is omitted. See POST /api/actions/web/webhook-send for one-shot fire-and-forget posts that ignore the response body.\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-of-string), \u0022body\u0022 (String), and \u0022timeout\u0022 (Number, milliseconds). Supply at least one of \u0022url\u0022 or \u0022uri\u0022 for a deliverable request.",
          "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."
          },
          "422": {
            "description": "Action failed - e.g., network error or timeout.",
            "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/remote-trigger": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send a remote trigger",
        "description": "Returns 200 with {ok: true, action: \u0022remote.trigger\u0022} on success.\r\nReturns 422 ACTION_FAILED if no listener is registered for the ID or the bridge cannot deliver, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe \u0022id\u0022 is matched against listeners registered by remote tools or companion processes. See POST /api/actions/web/webhook-send for delivering to external HTTP endpoints instead.\r\n            \r\n*Example: When a chat command fires a \u0022switch overlay\u0022 graph, send a remote trigger to a companion overlay process so its window changes scene without going through HTTP.*",
        "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."
          },
          "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/web/webhook-send": {
      "post": {
        "tags": [
          "L2 Web: Actions"
        ],
        "summary": "Send a webhook",
        "description": "Returns 200 with {ok: true, action: \u0022webhook.send\u0022} on success.\r\nReturns 422 ACTION_FAILED on network error or non-2xx response from the target, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nThe payload is sent as raw text. See POST /api/actions/web/http-request when the response body matters for downstream nodes.\r\n            \r\n*Example: When a donation event lands, POST a JSON {donor, amount} payload to a Discord webhook so the streamer\u0027s mod channel mirrors on-stream alerts.*",
        "operationId": "Actions_WebhookSend",
        "requestBody": {
          "description": "Optional dictionary with keys \u0022uri\u0022 (String) and \u0022payload\u0022 (String). Both keys are honored by the runtime; clients should supply at least \u0022uri\u0022 for a deliverable request.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "text/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "application/*\u002Bjson": {
              "schema": {
                "type": "object",
                "additionalProperties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook sent."
          },
          "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/webhook/listen": {
      "post": {
        "tags": [
          "L2 Webhook: Actions"
        ],
        "summary": "Register a webhook listener",
        "description": "Returns 200 with {ok: true, action: \u0022webhook.listen\u0022, listenerId: \u003Cguid\u003E, url: \u003Cstring\u003E, path: \u003Cstring\u003E, createdAt: \u003Ciso8601\u003E} on success.\r\nReturns 422 ACTION_FAILED if the path is malformed or already registered, and 503 OVERMOX_NOT_CONNECTED if no OverMox runtime is connected.\r\nInbound payloads arrive later via the L3 event webhook.received whose data carries the request body 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 \u0022/donations\u0022 so the service can POST alerts directly into a graph that fires confetti on screen.*",
        "operationId": "Actions_WebhookListen",
        "requestBody": {
          "description": "Required dictionary with key \u0022path\u0022 (String, URL path beginning with \u0022/\u0022).",
          "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."
          },
          "422": {
            "description": "Action failed - e.g., invalid or duplicate 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/webhook/unlisten": {
      "post": {
        "tags": [
          "L2 Webhook: Actions"
        ],
        "summary": "Remove a webhook listener",
        "description": "Returns 200 with {ok: true, action: \u0022webhook.unlisten\u0022} on success.\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 {ok: true, action: \u0022webhook.listeners\u0022, listeners: [{listenerId: \u003Cguid\u003E, url: \u003Cstring\u003E, path: \u003Cstring\u003E, createdAt: \u003Ciso8601\u003E}]} on success.\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 {ok: true, action: \u0022world.toViewport\u0022, x: Float, y: Float, z: Float} on success, where x and y are normalized viewport coordinates and z is the depth in world units from the camera.\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\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.",
            "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, including channels, operations, message schemas, and the shared EventEnvelope shape (Type, Timestamp, Data).\r\nThe document is regenerated from the sync registry by \u0060python -m tools.sync fix\u0060; if it has not been generated yet, 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 Twitch overlay that subscribes to chat-triggered scene events, fetch this catalog at startup to validate the EventEnvelope 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 has not been generated. Run \u0060python -m tools.sync fix\u0060 from the repo root. Then restart the OverMox Controller.",
            "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).",
            "nullable": true,
            "example": "/api/actions/scene/spawn"
          }
        },
        "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"
        ],
        "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
          },
          "locationY": {
            "type": "number",
            "description": "Vertical position to place the node on the graph canvas (required).",
            "format": "double",
            "example": 200
          }
        },
        "additionalProperties": false,
        "description": "Request to add a new node to a graph."
      },
      "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.3333334
          },
          "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": {
        "type": "object",
        "properties": {
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AddNodeRequest"
            },
            "description": "The list of node specifications to create. 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": {
        "type": "object",
        "properties": {
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PropertySetItem"
            },
            "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."
      },
      "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."
      },
      "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
          },
          "locationY": {
            "type": "number",
            "description": "Vertical position of the node on the graph canvas.",
            "format": "double",
            "example": 200
          },
          "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"
          },
          "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"
          }
        },
        "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"
          }
        },
        "additionalProperties": false,
        "description": "Request to open an OverMox project from the file system."
      },
      "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
          },
          "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."
      },
      "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": {
        "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": {
        "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."
      },
      "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": {
        "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
          },
          "locationY": {
            "type": "number",
            "description": "New vertical position on the graph canvas (optional).",
            "format": "double",
            "nullable": true,
            "example": 250
          }
        },
        "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
          }
        },
        "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": "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: Variables",
      "description": "Layer 1 REST mutations on global variables shared across graphs. Use these to coordinate state between graphs."
    },
    {
      "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: Batch",
      "description": "Layer 1 batched-operation entry point. Bundle several Layer 1 mutations into a single round trip."
    },
    {
      "name": "L1: Assets",
      "description": "Layer 1 property reads on bundled assets in the project."
    },
    {
      "name": "L1: Objects",
      "description": "Layer 1 property reads on scene objects in the loaded project."
    },
    {
      "name": "L1: Console",
      "description": "Layer 1 console log reads. Inspect recent log lines emitted by the OverMox runtime."
    },
    {
      "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 Math: Actions",
      "description": "Layer 2 math operations. Pure-function arithmetic, vector, and curve helpers."
    },
    {
      "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 EventEnvelope messages as they fire. See /asyncapi for the full event catalog."
    },
    {
      "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": "Dev",
      "description": "Internal and debug endpoints. Not for production use."
    }
  ],
  "servers": [
    {
      "url": "http://localhost:5000",
      "description": "Local OverMox API"
    }
  ]
}