{
  "openapi": "3.0.3",
  "info": {
    "title": "Wowza Video Intelligence Framework API",
    "version": "2.0.0",
    "description": "This reference documents every endpoint in the Wowza Video Intelligence Framework (VIF)\nREST API, including the parameters each one takes and the request and response bodies it\nworks with. VIF adds real-time AI video analysis, such as object detection and scene\nrecognition, to streams running on Wowza Streaming Engine, and this API is served by the\nVideo Intelligence Controller (VIC), a module that runs alongside the Engine's own REST\ninterface, on port 8087 by default.\n\nAlmost everything you do with this API comes down to one idea: a **config**, the group\nof settings that tells VIF how to analyze a stream (which detector to run, what to do\nwith the results, and so on). You'll run into a config in three places, and each one\nuses it a little differently. A **stream group config** applies a config to every\nstream whose name matches a pattern you choose, such as `cam.*`. An **override** applies\na config to one specific stream, by its exact name instead of a pattern. A running\n**stream** reports the config it's actually using right now, alongside its live\nconnection state and performance. Wherever you see one of these documents in the\nreference below, look for a `config` member: that's where the settings live. The rest\nof the document, the match rule, the stream's identity, its live state, just says how\nthat config gets used, and isn't part of the config itself.\n\nConfigs also build on each other in layers: a stream's final settings come from a\ndefault config, then a matching stream group config, then a per-stream override, then\nany changes made directly to the running stream. Each layer can fill in whatever the\none below it left unset. The `Config` schema, wherever it appears in this reference,\nexplains exactly how that merge works, including the two members, `detector` and\n`listeners`, that don't merge field by field like the rest.\n\nEndpoints are grouped below by what they work with: durable configuration you save to\ndisk, the live streams currently running, one-off video analysis jobs, connectivity\nchecks, and framework-wide status. Each group's description explains what you'll find\nin it.\n\nA few things are worth knowing before you make your first request. Every error this API\nreturns is `application/problem+json` (RFC 7807), so `error.title`, `error.status`, and\n`error.detail` tell you what went wrong; the exceptions are failures the API never gets\nto handle, like a URL the Engine can't route or an Engine-level authentication or\nlicense failure, which come back in the Engine's own `{success, code, message}` format\ninstead. Every document you can edit under `/persist` comes with an `ETag` header;\nsend that value back as `If-Match` when you write, and the API answers `428` if you\nforget it and `412` if it's gone stale, so you never overwrite a change you haven't\nseen yet. PATCH requests are a JSON Merge Patch (RFC 7386), not a full replacement: send\nonly the fields you want to change, and anything you leave out keeps its current value.\nSending a field as `null` removes it instead, letting it inherit from whatever's\nunderneath, except on a running stream, where `null` isn't accepted for `config` or\n`config.active`, because a running instance needs values to actually run with; use\n`POST .../reset` there if you want to discard your changes instead. The verbs you'll use\nare GET, POST, PATCH, and DELETE: POST creates a document (`409` if one already exists\nby that name) or triggers an action, like resetting a stream or resuming a job; PATCH\nedits a document that already exists (`404` if it doesn't); DELETE removes one. This API\nhas no PUT. Wherever an endpoint isn't JSON, uploading or downloading a video file, for\nexample, its description says so explicitly; everything else you send or receive is\nJSON.\n\n<blockquote><strong>Note</strong>: This is version 2 of the VIF REST API. Version 1 is\ndeprecated, and we strongly recommend moving any integration that writes configuration\nto v2. Saving through v1 rewrites your configuration files in an older format: a\nper-stream override saved that way loses what made it an override, and comes back\ninstead as a stream group config matched to that one stream's literal name. Reading\nthrough v1 stays safe at any time; just make sure every integration that <em>writes</em>\nconfiguration moves to v2, and that you don't mix the two on one installation.</blockquote>\n\n## Authentication\n\nEngine REST credentials (`engineBasic`). Requests are additionally subject to the Engine\nlicense entitlement (402 without it) and Engine RBAC (the \"basic\" role is read-only).\n"
  },
  "servers": [
    {
      "url": "{scheme}://{host}:{port}/v2/vif",
      "variables": {
        "scheme": {
          "default": "http",
          "enum": [
            "http",
            "https"
          ]
        },
        "host": {
          "default": "localhost"
        },
        "port": {
          "default": "8087"
        }
      }
    }
  ],
  "security": [
    {
      "engineBasic": []
    }
  ],
  "tags": [
    {
      "name": "Server",
      "description": "Framework-wide status: which VIS instances are connected, which streams are running, and which detection models are available on them. These endpoints are read-only."
    },
    {
      "name": "Runtime",
      "description": "The stream instances currently running. A GET here shows a stream's live, resolved configuration alongside its health and performance. Writes are ephemeral: they change only the running instance, are never saved to disk, and are lost if the stream or the Engine restarts. A `404` on any Runtime endpoint means the stream isn't currently running, not that it doesn't exist at all."
    },
    {
      "name": "Persist",
      "description": "The durable configuration that lives on disk whether or not a stream is running: stream group configs, which apply automatically to any stream whose name matches a pattern, and per-stream overrides. Writing here saves the document, and if a running stream is governed by it, applies the change to that stream immediately. You can also write an override for a stream that isn't running yet; it takes effect as soon as the stream starts."
    },
    {
      "name": "Probes",
      "description": "Connectivity checks for external endpoints, such as the vision-language model a detector calls out to. Use these before you save a configuration, to confirm VIF can actually reach the endpoint you're pointing it at."
    },
    {
      "name": "VOD",
      "description": "On-demand analysis: the video files under the Engine's content directory, and the jobs that analyze them. Unlike a running stream, a job outlives its analysis and is neither a config nor a stream. It just names the config it should use when you submit it, and keeps a record of what it actually used once it's done."
    }
  ],
  "paths": {
    "/status": {
      "get": {
        "tags": [
          "Server"
        ],
        "operationId": "getServerStatus",
        "summary": "Framework status",
        "description": "Host info, running streams and connected VIS instances.",
        "responses": {
          "200": {
            "description": "Current framework status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerStatus"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/models": {
      "get": {
        "tags": [
          "Server"
        ],
        "operationId": "getModelCatalog",
        "summary": "Available detection models",
        "description": "Detection models available on the connected VIS instances.",
        "responses": {
          "200": {
            "description": "Model catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelCatalog"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/listener-types": {
      "get": {
        "tags": [
          "Server"
        ],
        "operationId": "listListenerTypes",
        "summary": "Available listener types",
        "description": "The listener implementations discovered on this Engine — what a config UI offers as the choices for a custom listener's `class_name`. Advisory, not a gate: discovery scans the Engine's own lib path, and a class it misses can still run.",
        "responses": {
          "200": {
            "description": "The loaded listener types.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ListenerType"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/streams": {
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "listRuntimeStreams",
        "summary": "List running streams",
        "parameters": [
          {
            "name": "application",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only streams in this Engine application; absent lists every application."
          },
          {
            "name": "detector",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/DetectorType"
            },
            "description": "Only streams running this kind of analysis."
          },
          {
            "name": "active",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Filter on whether analysis runs. A published stream with `active: false` is listed but analyzes nothing."
          }
        ],
        "responses": {
          "200": {
            "description": "Running streams matching the filters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Stream"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "getRuntimeStream",
        "summary": "One running stream",
        "description": "Effective config, health and performance for the stream.",
        "responses": {
          "200": {
            "description": "The stream's resolved config and live state.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Runtime"
        ],
        "operationId": "updateRuntimeStream",
        "summary": "Update stream settings (ephemeral)",
        "description": "JSON Merge Patch over the stream's config, applied to the running instance and never\nsaved: a stream or Engine restart reverts it. The body is `{\"config\": {…}}`; a member\nat the top level other than `config` (and the `etag` a listed document carries, which\nis ignored) is a `400`. `config.detector` and `config.listeners` have their own\nsubresources; naming either one here is a `409`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/StreamSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stream after the change.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}/reset": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "post": {
        "tags": [
          "Runtime"
        ],
        "operationId": "resetRuntimeStream",
        "summary": "Discard the stream's ephemeral changes",
        "description": "Action endpoint: reverts the running instance to its persisted configuration\n(stream group config plus override) and applies it live, immediately — restarting the\ndetector session when the change calls for it, never the stream itself. Returns the\nresulting stream — fresh state and `ETag`, so the client needs no follow-up GET.\nIdempotent — succeeds whether or not ephemeral changes exist. No `If-Match`: this is\nan action, not a conditional edit.\n",
        "responses": {
          "200": {
            "description": "The stream after the reset.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stream"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}/detector": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "getRuntimeDetector",
        "summary": "Effective detector",
        "responses": {
          "200": {
            "description": "Detector.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detector"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Runtime"
        ],
        "operationId": "updateRuntimeDetector",
        "summary": "Update detector fields (ephemeral)",
        "description": "JSON Merge Patch within the detector, memory only. A patch that changes `type`\nreplaces the whole detector section. `404` when the stream is not running or runs\nno detector.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/Detector"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied detector.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}/listeners": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "listRuntimeListeners",
        "summary": "Effective listeners",
        "responses": {
          "200": {
            "description": "Listeners, keyed by name.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Listener"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "Runtime"
        ],
        "operationId": "createRuntimeListener",
        "summary": "Add a listener (ephemeral)",
        "description": "The listener's name travels in the body's `name` member, required here (`400`\nwithout it). `409` when a listener with that name already exists.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Listener"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Listener created on the running instance.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}/listeners/{name}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        },
        {
          "$ref": "#/components/parameters/ListenerName"
        }
      ],
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "getRuntimeListener",
        "summary": "One effective listener",
        "responses": {
          "200": {
            "description": "Listener.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Runtime"
        ],
        "operationId": "updateRuntimeListener",
        "summary": "Update listener fields (ephemeral)",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/Listener"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied listener.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "Runtime"
        ],
        "operationId": "deleteRuntimeListener",
        "summary": "Remove a listener (ephemeral)",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Listener removed from the running instance."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/runtime/apps/{app}/streams/{stream}/thumbnail": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Runtime"
        ],
        "operationId": "getThumbnail",
        "summary": "Thumbnail image of the running stream",
        "description": "The latest decoded frame, or a given one. `404` when the stream is not running or has produced no frame yet.",
        "parameters": [
          {
            "name": "width",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Target width in pixels; 0, the default, keeps the source width."
          },
          {
            "name": "height",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Target height in pixels; 0, the default, keeps the source height."
          },
          {
            "name": "fit",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "letterbox",
                "fitwidth",
                "fitheight",
                "matchsource",
                "crop"
              ],
              "default": "letterbox"
            },
            "description": "How the frame is fitted when the requested size has another aspect ratio: `letterbox` pads, `crop` trims, `fitwidth`/`fitheight` scale to that side and let the other follow, `matchsource` keeps the source size."
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "jpg",
                "png"
              ],
              "default": "png"
            },
            "description": "Encoding of the returned image, which sets the response media type."
          },
          {
            "name": "frame_id",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64",
              "default": 0
            },
            "description": "0, the default, is the latest frame. A specific frame is served from the analysis buffer at its own size, so `width`, `height` and `fit` do not apply."
          },
          {
            "name": "overlay",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Render the detection overlay. Falls back to the `-vi` overlay rendition, then to the plain source frame, when the annotated frame is not available."
          }
        ],
        "responses": {
          "200": {
            "description": "Thumbnail image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/configs/default": {
      "description": "The one config that is not a member of a collection: it has no match rule, and it is the layer every stream inherits before any group or override applies. There is deliberately no `GET /persist/configs` -- this path is the whole of `/persist/configs`, and the named configs are listed at `/persist/stream-group-configs`.",
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getDefaultConfig",
        "summary": "Default config",
        "responses": {
          "200": {
            "description": "Default config.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DefaultConfig"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updateDefaultConfig",
        "summary": "Update fields of the default config",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/DefaultConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied default config.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DefaultConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/stream-group-configs": {
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "listStreamGroupConfigs",
        "summary": "List stream group configs",
        "description": "Each config applies to every stream whose application and name match its rule.",
        "parameters": [
          {
            "name": "application",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only groups whose match rule names this Engine application."
          }
        ],
        "responses": {
          "200": {
            "description": "The named stream group configs. The default config is not one of them — it has no match rule and lives at `/persist/configs/default`; per-stream overrides live under `/persist/apps/{app}/streams/{stream}`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/StreamGroupConfig"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "Persist"
        ],
        "operationId": "createStreamGroupConfig",
        "summary": "Create a stream group config",
        "description": "`name`, `match.application` and `match.stream_pattern` are required (`400` without\nthem). `409` when a stream group config already answers to that name, and for the reserved\nname `default`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StreamGroupConfig"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stream group config created.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamGroupConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/stream-group-configs/{name}": {
      "parameters": [
        {
          "name": "name",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Stream group config name. `default` is reserved for the default config."
        }
      ],
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getStreamGroupConfig",
        "summary": "One stream group config",
        "responses": {
          "200": {
            "description": "Stream group config.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamGroupConfig"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updateStreamGroupConfig",
        "summary": "Update fields of a stream group config",
        "description": "The match rule can be rewritten freely; the name cannot, because it is the group's\nidentity — a patch that changes it is a `409`. A patch that leaves the group\nwithout `match.application` or `match.stream_pattern` is a `400`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/StreamGroupConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied stream group config.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamGroupConfig"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "Persist"
        ],
        "operationId": "deleteStreamGroupConfig",
        "summary": "Delete a stream group config",
        "description": "The streams it governs fall back to what the remaining layers resolve to, without republishing. `409` for the reserved `default` config, which cannot be deleted.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Stream group config deleted."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/stream-config-overrides": {
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "listStreamConfigOverrides",
        "summary": "List the saved per-stream overrides",
        "description": "Every stored override at once, so a client can enumerate saved configuration without knowing the stream names in advance. Each entry carries its stream's identity in `application` and `stream` and its own `etag`; the documents themselves are read and written one at a time under `/persist/apps/{app}/streams/{stream}`.",
        "parameters": [
          {
            "name": "application",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only overrides for streams of this Engine application."
          }
        ],
        "responses": {
          "200": {
            "description": "The saved overrides, in stored order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/StreamConfigOverride"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/apps/{app}/streams/{stream}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getStreamConfigOverride",
        "summary": "The stream's saved override document",
        "description": "`404` means the stream has no override — it follows its stream group config entirely.",
        "responses": {
          "200": {
            "description": "Override document.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamConfigOverride"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "Persist"
        ],
        "operationId": "createStreamConfigOverride",
        "summary": "Create the stream's override",
        "description": "Creates the override document and applies it if the stream is running; `409` when\nthe stream already has one. Works for streams that are not running — the override\ntakes effect when the stream starts. Edits go through PATCH.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StreamConfigOverride"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Override created.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamConfigOverride"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updateStreamConfigOverride",
        "summary": "Update fields of the stream's override",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/StreamConfigOverride"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied override.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StreamConfigOverride"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "Persist"
        ],
        "operationId": "deleteStreamConfigOverride",
        "summary": "Delete the stream's override",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Override deleted; the stream follows its stream group config again."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/apps/{app}/streams/{stream}/detector": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getPersistedDetector",
        "summary": "The detector saved in the stream's override",
        "description": "`404` means the override does not set a detector — its stream group config's applies.",
        "responses": {
          "200": {
            "description": "Detector.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detector"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updatePersistedDetector",
        "summary": "Update saved detector fields",
        "description": "JSON Merge Patch within the saved detector. A patch that changes `type` replaces\nthe whole detector section. `404` when the override sets no detector — add one by\npatching the override document with `{\"config\": {\"detector\": {...}}}`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/Detector"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied detector.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "Persist"
        ],
        "operationId": "deletePersistedDetector",
        "summary": "Remove the override detector",
        "description": "The stream inherits its stream group config's detector instead.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Detector removed from the override."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/apps/{app}/streams/{stream}/listeners": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        }
      ],
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "listPersistedListeners",
        "summary": "The saved override listeners",
        "description": "`404` when the stream has no override, like its sibling aspects.",
        "responses": {
          "200": {
            "description": "Listeners.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Listener"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "Persist"
        ],
        "operationId": "createPersistedListener",
        "summary": "Create a saved listener (and apply if running)",
        "description": "The listener's name travels in the body's `name` member, required here (`400`\nwithout it). `409` when a listener with that name already exists. Creates the\noverride when the stream has none.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Listener"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Listener created.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/apps/{app}/streams/{stream}/listeners/{name}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/App"
        },
        {
          "$ref": "#/components/parameters/StreamName"
        },
        {
          "$ref": "#/components/parameters/ListenerName"
        }
      ],
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getPersistedListener",
        "summary": "One saved listener",
        "responses": {
          "200": {
            "description": "Listener.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updatePersistedListener",
        "summary": "Update saved listener fields",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/Listener"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied listener.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listener"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "Persist"
        ],
        "operationId": "deletePersistedListener",
        "summary": "Remove a saved listener",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Listener removed from the override."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/vod-settings": {
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getVodSettings",
        "summary": "The VOD settings document",
        "description": "How this Engine runs on-demand analysis: how many jobs run at once and how many are kept, how long a finished job stays, where the content and the job records live, the default lifecycle webhook and its credential, whether jobs resume on their own, and the upload cap. Stored in its own file, `conf.modules/vif/vod/settings.json`, beside the stream configuration and never part of it.",
        "responses": {
          "200": {
            "description": "The settings.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodSettings"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updateVodSettings",
        "summary": "Edit the VOD settings",
        "description": "A merge patch, applied to the running registry at once — a smaller `max_jobs` evicts, a new `job_ttl_seconds` reschedules retention, a new webhook or default takes effect for the next job. `content_dir` and `jobs_dir` are read at Engine start and apply at the next one. `400` for a bound violation or a `lifecycle_webhook_secret` naming no configured secret.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/VodSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The applied settings.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodSettings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/persist/secrets": {
      "get": {
        "tags": [
          "Persist"
        ],
        "operationId": "getSecrets",
        "summary": "The names of the configured secrets",
        "description": "The credentials a job's lifecycle webhook may be authorized with, by name only — a value never leaves the Engine. Stored in `conf.modules/vif/vod/secrets.json`.",
        "responses": {
          "200": {
            "description": "The secret names.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Secrets"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "patch": {
        "tags": [
          "Persist"
        ],
        "operationId": "updateSecrets",
        "summary": "Set, rotate or remove secrets",
        "description": "A merge patch over `values`: a string sets or rotates the named secret, `null` removes it, a name not mentioned is kept. The revision covers the values, so a rotation changes the `ETag` even though the answer shows names only. `400` when a removal would leave the VOD settings' `lifecycle_webhook_secret` naming nothing.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/merge-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/Secrets"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The names after the patch.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Secrets"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "412": {
            "$ref": "#/components/responses/Problem412"
          },
          "428": {
            "$ref": "#/components/responses/Problem428"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/probes/vlm-endpoint": {
      "post": {
        "tags": [
          "Probes"
        ],
        "operationId": "probeVlmEndpoint",
        "summary": "Check connectivity to an OpenAI-compatible VLM endpoint",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VlmEndpointProbeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result (reachable or not).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VlmEndpointProbeResult"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/files": {
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "listVodFiles",
        "summary": "List the files a VOD job can analyze",
        "description": "Every analyzable container under the Engine's content directory, as the relative paths a job names its `file` by, newest first. At most 500 entries are listed; `truncated: true` marks a listing that was cut off, and the newest files are the ones kept. A listing is not a probe: a file here can still turn out to carry no H.264 track, which the job reports when it runs.",
        "responses": {
          "200": {
            "description": "The analyzable files.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodFileList"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Problem503"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "VOD"
        ],
        "operationId": "createVodFile",
        "summary": "Upload a source file into the content directory",
        "description": "The request body is the file itself; the target name rides the query string because\nnames carry subdirectory slashes. The bytes are written beside the target and renamed\ninto place, so a partial upload is never visible under an analyzable name, and the file\nis in the listing — and submittable — the moment this answers.\n\nUploads never overwrite: `409` when the name is taken. `400` for no name, an absolute\nor escaping path, a non-analyzable extension, or a path segment a file already\noccupies. `413` for an upload larger than the VOD settings' `max_upload_bytes`.\n",
        "parameters": [
          {
            "name": "file",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Where to store the upload, relative to the content directory, with an analyzable extension (`.mp4`, `.m4v`, `.mov`, `.f4v`). Subdirectories are created as needed."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "video/mp4": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The stored file, as the listing names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodFile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "413": {
            "$ref": "#/components/responses/Problem413"
          },
          "503": {
            "$ref": "#/components/responses/Problem503"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "VOD"
        ],
        "operationId": "deleteVodFile",
        "summary": "Remove a source file from the content directory",
        "description": "The opposite of the upload: the file `?file=` names, exactly as the listing spells it,\nis removed. Nothing else goes with it — no job record, stored rows or thumbnail, an\nemptied subdirectory stays, and a symbolic link is removed as the link, never what it\npoints at.\n\nFiles are not owned by jobs. A finished job keeps its record and results without its\nsource; a failed or cancelled job on a removed file can no longer be resumed — the\nresume answers `409` saying the source cannot be resolved, and automatic resume stands\ndown. `409` while a queued or running job is using the file, naming the job: cancel it\nfirst, then delete. `404` when no such file is there. `400` for no name, an absolute or\nescaping path, or a non-analyzable extension — the content directory is the Engine's\nplayback directory, and this removes only what the listing could show.\n",
        "parameters": [
          {
            "name": "file",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The file to remove, relative to the content directory, as the listing names it."
          }
        ],
        "responses": {
          "204": {
            "description": "File removed."
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "503": {
            "$ref": "#/components/responses/Problem503"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs": {
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "listVodJobs",
        "summary": "List the jobs this Engine knows about",
        "description": "Newest first, one page at a time. The `tag` and `state` filters are applied before the page is taken, so `total` is what they selected and `?state=pending&limit=1` is a queue depth. A listing is a reading, not a subscription: a job can leave the state it was selected for before the answer is read. Finished jobs stay listed until the retention settings evict them or a `DELETE` removes them.",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only jobs submitted with exactly this tag."
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "A comma-separated list of `VodJobState` names, in any case; only jobs in one of them. A name that is not a state is a `400`, never a silently widened listing."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "How many of the selected jobs to skip, newest first; below zero clamps to zero."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            },
            "description": "Page size, clamped to `[1, 1000]`."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of jobs, and the arithmetic to walk the others.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodJobPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "post": {
        "tags": [
          "VOD"
        ],
        "operationId": "createVodJob",
        "summary": "Submit a file for analysis",
        "description": "Queues an offline analysis of `file`, a path under the content directory as `GET\n/vod/files` lists it, and answers as soon as the job is queued; progress is polled from\nthe job. The job's configuration is resolved when it is submitted, in the same layers a\nstream's is: `default config < stream_group_config < config`. `stream_group_config`\nnames a stream group config (its match rule plays no part — the job uses its config);\n`config` is an inline config layered over it, or over the default config alone. At\nleast one of the two is required; both are allowed. The layering rules are the\ncontract's: a `detector` declared at a layer replaces the whole detector below it,\n`listeners` layer per entry, everything else field by field. What the job resolved to\nis recorded on it (`effective_config` on the single-job view) and a later edit of the\ngroup never touches a job already submitted.\n\n`400` for anything wrong with the body: a missing or unsupported `file`, an unknown\n`stream_group_config`, neither configuration member, a configuration that selects no\ndetector or is inactive, no runnable listener together with `store_results: false`,\nan unusable `lifecycle_webhook` URL, a `lifecycle_webhook_secret` naming no configured\nsecret. `503` when VOD is unavailable on this Engine.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VodJobRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The job as it was queued (without `config` and `effective_config`; read the job for those).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "503": {
            "$ref": "#/components/responses/Problem503"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "getVodJob",
        "summary": "One job",
        "description": "Includes both `config` (as submitted) and `effective_config` (as resolved and run).",
        "responses": {
          "200": {
            "description": "The job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodJob"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      },
      "delete": {
        "tags": [
          "VOD"
        ],
        "operationId": "deleteVodJob",
        "summary": "Remove a finished job",
        "description": "Removes the job's record, stored rows and thumbnail. Only a job in a terminal state can be removed: `409` for one still queued or running (cancel it first), for one that has just ended and is still writing its record (try again), and for one that was resumed while the removal waited. A removal is never reported that did not happen.",
        "responses": {
          "204": {
            "description": "Job removed."
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "post": {
        "tags": [
          "VOD"
        ],
        "operationId": "cancelVodJob",
        "summary": "Stop a queued or running job",
        "description": "The job settles to `cancelled` off this request and keeps its record and stored rows; removing those is what `DELETE` is for. `409` for a job already in a terminal state — a cancel that raced completion says the job completed rather than pretend it stopped anything.",
        "responses": {
          "202": {
            "description": "Accepted; the job as it stood when the cancel was taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodJob"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}/resume": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "post": {
        "tags": [
          "VOD"
        ],
        "operationId": "resumeVodJob",
        "summary": "Resume a failed or cancelled job",
        "description": "The same job id runs again from where its stored results stop, appending to the same\nresults file rather than analyzing the source afresh; `resumes` counts the runs. A job\nsubmitted with a `stream_group_config` reloads that group by name and re-layers the\ninline `config` it was submitted with — the group must still resolve to the same\nanalysis, or the resume is refused. An inline-only job whose credentials were redacted\nout of its record takes them from the body: `config` must be the same analysis as\nsubmitted, and only its credentials are taken. A body on a group-built job is refused.\n\n`400` for a body this API cannot read. `409` for every other refusal, each saying which:\na job still queued or running, a completed job with nothing to fill in, a job that kept\nno results, a source file that changed since the job ran, a configuration that no\nlonger matches, credentials the record does not hold and the body did not supply, a\nresume point this file cannot open a window on, a job still writing its record (try\nagain).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VodJobResumeRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted; the job as it was queued again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "409": {
            "$ref": "#/components/responses/Problem409"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}/results": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "listVodJobResults",
        "summary": "The job's stored detections, one page at a time",
        "description": "Every response the analysis service returned for the job, as the job stored it — media-time stamped and unfiltered by any listener's gating — verbatim rows, never reshaped. A job still running serves what it has answered so far. `?from_ms&to_ms` narrow the page to a half-open stretch of the source. `404` when the job is unknown, when it was submitted with `store_results: false`, or when it has not stored a row yet; the `detail` says which.",
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Rows to skip; below zero clamps to zero."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            },
            "description": "Page size, clamped to `[1, 1000]`."
          },
          {
            "name": "from_ms",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only rows whose window starts at or after this media time."
          },
          {
            "name": "to_ms",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only rows whose window starts before this media time."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of rows, and the arithmetic to walk the others.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VodResultsPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem400"
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}/results/file": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "getVodJobResultsFile",
        "summary": "The job's detections as NDJSON",
        "description": "The results file itself, one JSON object per line, as an attachment named `<jobId>.jsonl`. A job whose results were compressed at rest is served verbatim under `Content-Encoding: gzip` to a client that accepts it, and decompressed to one that does not; either way what arrives is the same lines. `404` as for the paged results.",
        "responses": {
          "200": {
            "description": "The results file.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/vod/jobs/{jobId}/thumbnail": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "tags": [
          "VOD"
        ],
        "operationId": "getVodJobThumbnail",
        "summary": "The job's frame (in progress or final)",
        "description": "The job's own decoded frame — the size, scaling and encoding the detector was given — so it shows what was analyzed rather than a re-render of the source. `404` when the job is unknown or has no frame to show, which is every job on the clip path (a synthetic detector relays encoded video and never decodes a picture).",
        "responses": {
          "200": {
            "description": "The frame.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem404"
          },
          "default": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "engineBasic": {
        "type": "http",
        "scheme": "basic",
        "description": "Wowza Streaming Engine REST credentials (basic; digest is supported by the Engine but\nnot by browser-based generated clients). Subject to the Engine license entitlement\nand Engine RBAC.\n"
      }
    },
    "parameters": {
      "App": {
        "name": "app",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Engine application holding the stream (e.g. `live`). The vhost and application instance are the ones VIC serves and are not addressed here."
      },
      "StreamName": {
        "name": "stream",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "One stream's exact name, never a pattern — patterns belong to a stream group config's `match.stream_pattern`."
      },
      "ListenerName": {
        "name": "name",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Listener name (e.g. `Overlays`, `Webhooks`)."
      },
      "IfMatch": {
        "name": "If-Match",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "ETag of the revision this write is based on. `*` is not supported: quoting \"any revision\" defeats the concurrency check and is rejected with `412`."
      },
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "A VOD job's id, as `createVodJob` answered it."
      }
    },
    "headers": {
      "ETag": {
        "schema": {
          "type": "string"
        },
        "description": "Opaque revision tag of the returned document."
      }
    },
    "responses": {
      "Problem": {
        "description": "Error.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem400": {
        "description": "The request body is not what the operation takes: malformed JSON, a member the schema does not define, or a missing one it requires (a create without `name`) — or, for an upload, a body that is not the file or a target name the content directory cannot take.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ProblemName400": {
        "description": "The name in the path is not one this API can address — it carries a character the file-name mapping would rewrite, so the request names no document.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem404": {
        "description": "Not found.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem409": {
        "description": "Conflict with the current state or semantics of the resource.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem412": {
        "description": "Precondition failed (`If-Match` does not match the current revision).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem413": {
        "description": "The upload is larger than the VOD settings' `max_upload_bytes`.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem428": {
        "description": "Precondition required (`If-Match` header missing).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Problem503": {
        "description": "VOD is unavailable on this Engine: its content directory or its jobs directory could not be resolved or created. The VI log at startup says which.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem details.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "URI identifying the problem type."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary of the problem type."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code, repeated from the response line."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "instance": {
            "type": "string",
            "format": "uri",
            "description": "URI identifying this specific occurrence of the problem."
          }
        },
        "required": [
          "title",
          "status"
        ]
      },
      "DetectorType": {
        "type": "string",
        "enum": [
          "scene",
          "object",
          "vlm",
          "synthetic"
        ]
      },
      "Config": {
        "type": "object",
        "description": "The settings that make a stream analyze: which detector runs, what its listeners do\nwith the results, how frames are processed, which service analyzes them, and\ndiagnostics. Every document that carries one keeps it under a member literally named\n`config` — see \"Model\" in this API's description for how stream group configs,\noverrides and running streams each use one.\n\nExcept on a running stream, where every layer is already resolved, a config is\nsparse: an omitted member isn't a value, it inherits from the layer below. Most\nmembers merge field by field; `detector` replaces the whole section at once instead,\nand `listeners` merge per entry by name — see \"Model\" for why.\n",
        "properties": {
          "active": {
            "type": "boolean",
            "description": "Whether analysis runs."
          },
          "detector": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/Detector"
              }
            ],
            "description": "The stream's analysis; absent inherits from the layer below, explicit null selects none."
          },
          "listeners": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Listener"
            },
            "description": "Listeners, keyed by name. Each entry merges independently of the others."
          },
          "processing": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Processing"
              }
            ],
            "description": "How frames are sampled and shipped to analysis; absent inherits from the layer below."
          },
          "service": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ServiceBinding"
              }
            ],
            "description": "Which Video Intelligence Service analyzes the stream; absent inherits from the layer below."
          },
          "diagnostics": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Diagnostics"
              }
            ],
            "description": "Debug aids; absent inherits from the layer below."
          }
        }
      },
      "BaselineConfig": {
        "type": "object",
        "description": "The default config's config. Everything a config has except `detector`: the default config selects no detector type, and carries a per-type baseline for each of them in `detectors` beside it instead.",
        "properties": {
          "active": {
            "type": "boolean",
            "description": "Whether analysis runs, for every stream that does not say otherwise."
          },
          "listeners": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Listener"
            },
            "description": "Listeners, keyed by name, applied to every stream that does not override them."
          },
          "processing": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Processing"
              }
            ],
            "description": "Frame sampling and shipping settings applied to every stream that does not override them."
          },
          "service": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ServiceBinding"
              }
            ],
            "description": "Which Video Intelligence Service analyzes streams that do not override it."
          },
          "diagnostics": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Diagnostics"
              }
            ],
            "description": "Debug aids applied to every stream that does not override them."
          }
        }
      },
      "Stream": {
        "type": "object",
        "description": "A running stream under video intelligence: the config it resolved to, and the live state that is not configuration.",
        "properties": {
          "application": {
            "type": "string",
            "description": "Engine application the stream is published to."
          },
          "name": {
            "type": "string",
            "description": "The stream's name."
          },
          "vhost": {
            "type": "string",
            "description": "Engine virtual host holding the application, normally `_defaultVHost_`."
          },
          "instance": {
            "type": "string",
            "description": "Application instance the stream is published to, normally `_definst_`."
          },
          "state": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StreamState"
              }
            ],
            "description": "The stream's live connection state and performance counters."
          },
          "video": {
            "allOf": [
              {
                "$ref": "#/components/schemas/VideoInfo"
              }
            ],
            "description": "The source video's resolution, frame rate and GOP size."
          },
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "What the stream is running with — every layer already resolved, so nothing here is absent-and-inheriting the way a persisted document's members are."
          },
          "ephemeral_changes": {
            "type": "boolean",
            "readOnly": true,
            "description": "True while the running instance diverges from its persisted configuration (stream group config plus override) because of runtime writes. `POST .../reset` discards the divergence."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "This stream's configuration revision, to quote as `If-Match` on a write. Carried on the document so a client that listed streams can write one without reading it again; a single-stream read repeats it in the `ETag` header. It covers the config only, not the live counters under `state`. Server-assigned and ignored on a write."
          }
        },
        "required": [
          "application",
          "name",
          "config",
          "state"
        ]
      },
      "StreamSettings": {
        "type": "object",
        "description": "The runtime PATCH target: a config, and nothing else. `config.detector` and\n`config.listeners` are managed via their own subresources and are a `409` here. An\n`etag` member in the body is accepted and ignored, so a listed stream document can be\nwritten back as-is.\n",
        "properties": {
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The config to merge into the running instance."
          }
        }
      },
      "StreamState": {
        "type": "object",
        "description": "Runtime state, read-only.",
        "properties": {
          "connection": {
            "type": "string",
            "description": "VIS connection state (e.g. `connected`); stays `connected` during an AI-endpoint outage."
          },
          "endpoint_degraded": {
            "type": "boolean",
            "nullable": true,
            "description": "True while the stream's AI endpoint (VLM or SVD NIM) is unreachable."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Detail behind the current `connection` or `endpoint_degraded` state, when there is any."
          },
          "service_version": {
            "type": "string",
            "nullable": true,
            "description": "Version of the VIS instance serving this stream."
          },
          "performance": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/Performance"
              }
            ],
            "description": "Live performance counters; absent while the stream reports none yet."
          }
        },
        "required": [
          "connection"
        ]
      },
      "VideoInfo": {
        "type": "object",
        "description": "Source video properties, read-only.",
        "properties": {
          "width": {
            "type": "integer",
            "nullable": true,
            "description": "Source width in pixels."
          },
          "height": {
            "type": "integer",
            "nullable": true,
            "description": "Source height in pixels."
          },
          "frame_rate": {
            "type": "number",
            "nullable": true,
            "description": "Source frame rate, in frames per second."
          },
          "gop_size": {
            "type": "integer",
            "nullable": true,
            "description": "Source GOP (keyframe interval) size, in frames."
          }
        }
      },
      "Performance": {
        "type": "object",
        "description": "Live counters for the stream. The `*_avg` members are rolling averages in milliseconds; the three below them are not times and are noted individually.",
        "properties": {
          "ping_rtt_avg": {
            "type": "number",
            "nullable": true,
            "description": "Round-trip time to the VIS instance."
          },
          "preprocess_time_avg": {
            "type": "number",
            "nullable": true,
            "description": "Time preparing a frame before it reaches the model."
          },
          "inference_time_avg": {
            "type": "number",
            "nullable": true,
            "description": "Time the model spends on inference."
          },
          "postprocess_time_avg": {
            "type": "number",
            "nullable": true,
            "description": "Time turning inference output into detections."
          },
          "total_processing_time_avg": {
            "type": "number",
            "nullable": true,
            "description": "End-to-end time from frame capture to result."
          },
          "frame_detect_time_avg": {
            "type": "number",
            "nullable": true,
            "description": "Time spent specifically on the detection step."
          },
          "frame_window": {
            "type": "integer",
            "nullable": true,
            "description": "Length of one analysis window in seconds: the window duration for scene, VLM and synthetic; 1 for transcoder-fed object detection, or the frame-grab interval when it is fed by grabs."
          },
          "video_frames_total": {
            "type": "integer",
            "nullable": true,
            "description": "Frames taken from the source over the last 10 seconds, not since the stream started. A rolling count, so it falls back to 0 when analysis stops."
          },
          "frames_detected": {
            "type": "integer",
            "nullable": true,
            "description": "Frames analyzed over the same rolling 10 seconds, to read against `video_frames_total`. Lower means frames are being sampled or skipped, not lost."
          }
        }
      },
      "Detector": {
        "type": "object",
        "description": "The analysis running on a stream. `type` selects the subtype; a patch that changes\nit replaces the whole detector section.\n",
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectorType"
              }
            ],
            "description": "Detector subtype: `scene`, `object`, `vlm`, or `synthetic`."
          }
        },
        "required": [
          "type"
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "scene": "#/components/schemas/SceneDetector",
            "object": "#/components/schemas/ObjectDetector",
            "vlm": "#/components/schemas/VlmDetector",
            "synthetic": "#/components/schemas/SyntheticDetector"
          }
        }
      },
      "SceneDetector": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Detector"
          },
          {
            "type": "object",
            "description": "Whole-frame scene classification (fire, smoke, fighting, ...).",
            "properties": {
              "classes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Scene classes to detect."
              },
              "sensitivity": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10,
                "description": "Higher detects more; per-class overrides in `class_sensitivity`. Default 5."
              },
              "class_sensitivity": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 10
                },
                "description": "Per-class sensitivity overrides, keyed by class name; unlisted classes use `sensitivity`."
              },
              "min_confidence": {
                "type": "number",
                "minimum": 0,
                "maximum": 1,
                "description": "Detections below this confidence are dropped. Default 0.3."
              },
              "baseline": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SceneBaseline"
                  }
                ],
                "description": "The class set frame scores are normalized against."
              }
            }
          }
        ]
      },
      "SceneBaseline": {
        "type": "object",
        "description": "The class set a frame's scores are normalized against. Sensitivity is read as a distance from the mean score over this set, so it decides what counts as an unusual match — not which classes are reported, which is `classes`. Leave it alone unless the stream's subject matter makes the general-purpose baseline a poor reference.",
        "properties": {
          "set": {
            "type": "string",
            "nullable": true,
            "enum": [
              "DEFAULT",
              "TRAFFIC"
            ],
            "description": "Named baseline: `DEFAULT` is ~1560 general scenes, `TRAFFIC` ~190 traffic scenes. Null falls back to `custom_classes`."
          },
          "custom_classes": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Baseline classes given outright, used when `set` is null. Enough of them to describe what the camera normally sees, or scores normalize against too little."
          }
        }
      },
      "ObjectDetector": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Detector"
          },
          {
            "type": "object",
            "description": "Object detection with bounding boxes (RF-DETR).",
            "properties": {
              "classes": {
                "type": "array",
                "nullable": true,
                "items": {
                  "type": "string"
                },
                "description": "Classes to report; null reports all model classes."
              },
              "model": {
                "type": "string",
                "enum": [
                  "nano",
                  "small",
                  "medium",
                  "large"
                ],
                "description": "Model variant. Ignored when `checkpoint_path` is set. Default `medium`."
              },
              "checkpoint_path": {
                "type": "string",
                "nullable": true,
                "description": "Custom model weights on the VIS host; overrides `model`."
              },
              "min_confidence": {
                "type": "number",
                "minimum": 0,
                "maximum": 1,
                "description": "Detections below this confidence are dropped. Default 0.3."
              },
              "tracking": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Tracking"
                  }
                ],
                "description": "Absent or null inherits; `{\"method\": \"none\"}` turns tracking off."
              },
              "tiling": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Tiling"
                  }
                ],
                "description": "Absent or null inherits; `{\"mode\": \"none\"}` turns tiling off."
              }
            }
          }
        ]
      },
      "Tracking": {
        "type": "object",
        "description": "Persistent object IDs across frames. Absent inherits from the layer below, where the\nbuilt-in default is `byte-track`; turning tracking off takes `method: none`.\n",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "byte-track",
              "none"
            ],
            "description": "`byte-track` assigns persistent IDs; `none` reports each frame's detections on their own. The remaining members apply to `byte-track` only."
          },
          "min_confidence_to_create": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Confidence needed for a detection to start a new track."
          },
          "min_consecutive_frames": {
            "type": "integer",
            "minimum": 1,
            "description": "Frames an object must persist before its track becomes visible."
          },
          "min_overlap": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Overlap fraction for consecutive detections to match a track."
          },
          "max_lost_frames": {
            "type": "integer",
            "minimum": 0,
            "description": "Frames a track survives without detections."
          },
          "ignore_untracked": {
            "type": "boolean",
            "description": "Drop detections that have no track."
          }
        },
        "required": [
          "method"
        ]
      },
      "Tiling": {
        "type": "object",
        "description": "Slice frames into a grid to improve recall on small objects, at the cost of more\ninference. Absent inherits from the layer below, where the built-in default is\n`none`; turning tiling off takes `mode: none`. `max_grid` sizes the grid and is\nrequired for every mode but `none`.\n",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "none",
              "fixed",
              "gated"
            ],
            "description": "`none` runs no tiling; `fixed` runs one grid per frame; `gated` runs probe + coarse + fine grids."
          },
          "min_grid": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Grid"
              }
            ],
            "description": "Grid size and shape for the coarse/probe pass in `gated` mode."
          },
          "max_grid": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Grid"
              }
            ],
            "description": "Grid size and shape for the full-resolution tiling pass. Required for every mode but `none`."
          },
          "tile_coverage_cutoff": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Drop tile detections covering more than this fraction of the tile; 1.0 disables."
          },
          "full_frame_pass": {
            "type": "boolean",
            "description": "`fixed` mode: also run a full-frame pass for objects larger than a tile."
          },
          "cluster_suppression_min_children": {
            "type": "integer",
            "minimum": 0,
            "description": "Drop a detection containing at least this many smaller ones; 0 disables."
          }
        },
        "required": [
          "mode"
        ]
      },
      "Grid": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16,
            "description": "Number of tile rows."
          },
          "cols": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16,
            "description": "Number of tile columns."
          }
        },
        "required": [
          "rows",
          "cols"
        ]
      },
      "VlmDetector": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Detector"
          },
          {
            "type": "object",
            "description": "Free-text vision-language analysis against an OpenAI-compatible endpoint.\n`mode` names the analysis family explicitly; the per-mode blocks carry its\nsettings — `describe` has none, by design — and `endpoint` and `generation`\nare shared across modes.\n",
            "properties": {
              "endpoint": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/VlmEndpoint"
                  }
                ],
                "description": "Null inherits from the default config, and then the built-in `http://vlm.docker:8000/v1` with `Qwen/Qwen3-VL-4B-Instruct-FP8`."
              },
              "mode": {
                "type": "string",
                "enum": [
                  "detect",
                  "describe",
                  "custom"
                ],
                "description": "Which analysis family runs. Only the matching block below applies."
              },
              "detect": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/VlmDetectSettings"
                  }
                ],
                "description": "Settings for `mode: detect`; applies only when `mode` is `detect`."
              },
              "custom": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/VlmCustomSettings"
                  }
                ],
                "description": "Settings for `mode: custom`; applies only when `mode` is `custom`."
              },
              "generation": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GenerationParams"
                  }
                ],
                "description": "Sampling parameters shared across every mode."
              }
            },
            "required": [
              "mode"
            ]
          }
        ]
      },
      "VlmDetectSettings": {
        "type": "object",
        "description": "Detect mode — look for the given classes.",
        "properties": {
          "classes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Classes to look for."
          },
          "class_hints": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Per-class hints inlined into the prompt; keys must be members of `classes`."
          },
          "reasoning_level": {
            "type": "string",
            "nullable": true,
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "description": "Built-in prompt preset VIS applies server-side: `low` and `medium` select its own prompt pairs plus a per-class boolean schema; `high` or absent uses the default class-aware prompts."
          }
        },
        "required": [
          "classes"
        ]
      },
      "VlmCustomSettings": {
        "type": "object",
        "description": "Custom mode — caller-authored prompt and response contract.",
        "properties": {
          "prompt": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/Prompt"
              }
            ],
            "description": "Caller-authored prompt; null uses the endpoint's own default prompt."
          },
          "response_schema": {
            "type": "object",
            "nullable": true,
            "description": "JSON Schema forwarded verbatim to the endpoint."
          },
          "classes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Classes expanded into the `{class_list}` placeholder wherever either prompt uses it. Optional, and not a class list the analysis service filters on: in this mode the prompt decides what the model is asked, and these only render into it."
          },
          "class_hints": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Per-class hints inlined into `{class_list}`; keys must be members of `classes`."
          }
        }
      },
      "Prompt": {
        "type": "object",
        "description": "Placeholders: `{class_list}`, `{frame_count}`, `{duration_seconds}`.",
        "properties": {
          "system": {
            "type": "string",
            "nullable": true,
            "description": "System prompt text."
          },
          "user": {
            "type": "string",
            "nullable": true,
            "description": "User prompt text."
          }
        }
      },
      "GenerationParams": {
        "type": "object",
        "description": "Sampling parameters passed to the endpoint.",
        "properties": {
          "temperature": {
            "type": "number",
            "nullable": true,
            "description": "Sampling temperature passed to the endpoint."
          },
          "max_tokens": {
            "type": "integer",
            "nullable": true,
            "description": "Maximum tokens the endpoint may generate."
          }
        }
      },
      "VlmEndpoint": {
        "type": "object",
        "description": "Members absent or null inherit individually — an override may set just `model`.",
        "properties": {
          "url": {
            "type": "string",
            "nullable": true,
            "description": "OpenAI-compatible endpoint, e.g. `http://vlm.docker:8000/v1`."
          },
          "model": {
            "type": "string",
            "nullable": true,
            "description": "Model name to request from the endpoint."
          },
          "api_key": {
            "type": "string",
            "nullable": true,
            "description": "Never echoed by a read. On a write: absent or null keeps the stored value, an empty string clears it."
          },
          "timeout_seconds": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true,
            "description": "Request timeout, in seconds."
          },
          "max_concurrent_requests": {
            "type": "integer",
            "minimum": 1,
            "description": "Maximum requests sent to the endpoint at once."
          }
        }
      },
      "SyntheticDetector": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Detector"
          },
          {
            "type": "object",
            "description": "Synthetic / AI-generated video detection via an NVIDIA SVD NIM.",
            "properties": {
              "endpoint": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SvdEndpoint"
                  }
                ],
                "description": "Absent or null inherits from the default config."
              },
              "classification_threshold": {
                "type": "number",
                "minimum": 0,
                "maximum": 1,
                "default": 0.3,
                "description": "Verdict is `synthetic` when the score exceeds this."
              }
            }
          }
        ]
      },
      "SvdEndpoint": {
        "type": "object",
        "description": "Members absent or null inherit individually.",
        "properties": {
          "address": {
            "type": "string",
            "nullable": true,
            "description": "gRPC `host:port` (not an http URL), e.g. `svd.docker:8001`."
          },
          "tls": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/SvdEndpointTls"
              }
            ],
            "description": "TLS settings for the connection; null auto-detects."
          },
          "api_key": {
            "type": "string",
            "nullable": true,
            "description": "Never echoed by a read. On a write: absent or null keeps the stored value, an empty string clears it."
          },
          "function_id": {
            "type": "string",
            "nullable": true,
            "description": "NVIDIA Cloud Function ID, when the endpoint is served through NVCF."
          },
          "timeout_seconds": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true,
            "description": "Request timeout, in seconds."
          },
          "max_concurrent_requests": {
            "type": "integer",
            "minimum": 1,
            "description": "Maximum requests sent to the endpoint at once."
          }
        }
      },
      "SvdEndpointTls": {
        "type": "object",
        "description": "Null auto-detects (TLS on port 443 or when material is set).",
        "properties": {
          "enabled": {
            "type": "boolean",
            "nullable": true,
            "description": "Force TLS on or off; null auto-detects from the port and configured material."
          },
          "ca_cert": {
            "type": "string",
            "nullable": true,
            "description": "Path to the CA certificate on the VIS host, not the certificate itself (e.g. `/certs/ca-cert.pem`). Needed when the endpoint presents a private-CA certificate."
          },
          "client_cert": {
            "type": "string",
            "nullable": true,
            "description": "Path to the client certificate on the VIS host, for mutual TLS. Set it together with `client_key`; one without the other does not authenticate."
          },
          "client_key": {
            "type": "string",
            "nullable": true,
            "description": "Never echoed by a read. On a write: absent or null keeps the stored value, an empty string clears it."
          }
        }
      },
      "Listener": {
        "type": "object",
        "description": "What happens with detection results. `type` selects the subtype; `custom` is the\nescape hatch for customer modules.\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "overlay",
              "webhook",
              "id3",
              "log",
              "custom"
            ],
            "description": "Listener subtype: selects which fields beyond this apply."
          },
          "name": {
            "type": "string",
            "writeOnly": true,
            "description": "Creation only: the listener's key in the collection. Ignored on other operations."
          },
          "enabled": {
            "type": "boolean",
            "default": true,
            "description": "Whether the listener runs. Default true."
          },
          "trigger": {
            "type": "string",
            "enum": [
              "immediate",
              "batch",
              "rollup"
            ],
            "description": "When the listener fires — per detection, per batch, or per rollup window."
          },
          "min_confidence": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 1,
            "description": "Only forward detections at or above this confidence."
          },
          "suppress_empty": {
            "type": "boolean",
            "nullable": true,
            "description": "Skip events that contain no detections."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "This listener's revision, to quote as `If-Match` on a write. The collection response carries it per listener so a client that listed them can write one without reading it again; a single-listener read repeats it in the `ETag` header. Server-assigned and ignored on a write."
          }
        },
        "required": [
          "type"
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "overlay": "#/components/schemas/OverlayListener",
            "webhook": "#/components/schemas/WebhookListener",
            "id3": "#/components/schemas/Id3Listener",
            "log": "#/components/schemas/LogListener",
            "custom": "#/components/schemas/CustomListener"
          }
        }
      },
      "OverlayListener": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Listener"
          },
          {
            "type": "object",
            "description": "Renders detections onto a `-vi` overlay stream.",
            "properties": {
              "width": {
                "type": "integer",
                "nullable": true,
                "description": "Overlay stream width in pixels; null matches the source."
              },
              "height": {
                "type": "integer",
                "nullable": true,
                "description": "Overlay stream height in pixels; null matches the source."
              },
              "frame_rate": {
                "type": "number",
                "nullable": true,
                "description": "Overlay stream frame rate; null matches the source."
              },
              "overlay_delay": {
                "type": "integer",
                "nullable": true,
                "description": "How many initial detection events are skipped before overlays start rendering, to let the overlay stream align with the video. A count of events, not a time."
              },
              "fade_step": {
                "type": "integer",
                "nullable": true,
                "description": "Over how many frames an expiring box fades to invisible — larger fades slower. A whole number of frames."
              },
              "jitter": {
                "type": "integer",
                "nullable": true,
                "description": "Smoothing tolerance for box movement, in pixels."
              },
              "replace_video": {
                "type": "boolean",
                "nullable": true,
                "description": "Replace the video with the overlay instead of compositing."
              },
              "show_stats": {
                "type": "boolean",
                "nullable": true,
                "description": "Render the stats block on the overlay."
              },
              "debug_string": {
                "type": "string",
                "nullable": true,
                "description": "Title overlay, a date-format pattern with `{{app_name}}`, `{{stream_name}}`, `{{detector_type}}` variables."
              }
            }
          }
        ]
      },
      "WebhookListener": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Listener"
          },
          {
            "type": "object",
            "description": "Posts detection events to an HTTP endpoint.",
            "properties": {
              "url": {
                "type": "string",
                "nullable": true,
                "description": "Target URL; null uses the server-wide webhook target."
              }
            }
          }
        ]
      },
      "Id3Listener": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Listener"
          },
          {
            "type": "object",
            "description": "Injects detections as ID3 timed metadata into the stream."
          }
        ]
      },
      "LogListener": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Listener"
          },
          {
            "type": "object",
            "description": "Writes detection events to a log file.",
            "properties": {
              "file_name": {
                "type": "string",
                "nullable": true,
                "description": "Log file name; null uses a built-in default."
              },
              "file_path": {
                "type": "string",
                "nullable": true,
                "description": "Directory the log file is written to; null uses a built-in default."
              }
            }
          }
        ]
      },
      "CustomListener": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Listener"
          },
          {
            "type": "object",
            "description": "A customer-provided listener module.",
            "properties": {
              "class_name": {
                "type": "string",
                "description": "Java class implementing the listener."
              },
              "properties": {
                "type": "object",
                "additionalProperties": true,
                "description": "Module-specific settings, passed through verbatim. Values are free-form JSON (strings, numbers, booleans, arrays, objects)."
              }
            },
            "required": [
              "class_name"
            ]
          }
        ]
      },
      "Processing": {
        "type": "object",
        "description": "How frames are sampled and shipped to analysis.",
        "properties": {
          "inference_fps": {
            "type": "integer",
            "description": "Frames per second sent to analysis; -1 matches the source rate. Default -1."
          },
          "window_seconds": {
            "type": "number",
            "description": "Seconds per analysis window (scene/VLM/synthetic). Default 2."
          },
          "video_height": {
            "type": "integer",
            "description": "Resize frames to this height; -1 matches source, 0 matches the model. Default 0."
          },
          "grayscale": {
            "type": "boolean",
            "description": "Send frames in grayscale. Default false."
          },
          "frame_source": {
            "type": "string",
            "enum": [
              "transcoder",
              "grab"
            ],
            "description": "Capture frames via the transcoder or periodic frame grabs. Default `transcoder`, except the `synthetic` detector, which taps the source packets and defaults to `grab`."
          },
          "grab_interval_seconds": {
            "type": "number",
            "description": "Frame-grab period when `frame_source` is `grab`. Default 1."
          },
          "buffer_frames": {
            "type": "integer",
            "description": "Frames buffered awaiting processing. Sized from the incoming video by default."
          },
          "auto_throttle": {
            "type": "boolean",
            "description": "Reduce `inference_fps` automatically when inference falls behind. Default off."
          },
          "catch_up": {
            "type": "object",
            "nullable": true,
            "description": "Scene/VLM — skip stale backlog to the live edge when inference lags.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Default true."
              },
              "max_behind_seconds": {
                "type": "number",
                "description": "Default is the buffer's own headroom."
              }
            }
          },
          "rollup_interval_seconds": {
            "type": "number",
            "description": "How long detections are held for batch/rollup listener events. Default 2."
          },
          "gpu_ids": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "integer"
            },
            "description": "Pin VIS-side inference to these GPUs; null lets VIS balance."
          }
        }
      },
      "ServiceBinding": {
        "type": "object",
        "description": "Which Video Intelligence Service analyzes this stream.",
        "properties": {
          "url": {
            "type": "string",
            "description": "VIS WebSocket endpoint, e.g. `ws://vis-host:5001/ws/stream/`."
          },
          "api_key": {
            "type": "string",
            "nullable": true,
            "description": "Never echoed by a read. On a write: absent or null keeps the stored value, an empty string clears it."
          },
          "model_idle_timeout_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "How long VIS keeps an idle model loaded."
          }
        }
      },
      "Diagnostics": {
        "type": "object",
        "description": "Debug aids; all off by default.",
        "properties": {
          "save_images": {
            "type": "boolean",
            "description": "Save captured frames to disk on the Engine host. Default false."
          },
          "timing_log_seconds": {
            "type": "integer",
            "description": "Seconds between timing-metric dumps; 0 disables. Default 0."
          },
          "max_logged_messages": {
            "type": "integer",
            "description": "Protocol messages logged; -1 all, 0 disabled. Default 20."
          }
        }
      },
      "StreamConfigOverride": {
        "type": "object",
        "description": "One stream's saved config, layered over its stream group config's — a sparse config keyed to\nthat stream. Sparse by design: only the members it sets override the group's, and\neverything absent keeps inheriting. Exists independently of the stream running.\n",
        "properties": {
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The sparse config layered over the stream group config's."
          },
          "application": {
            "type": "string",
            "nullable": true,
            "description": "The stream's Engine application. Identity normally lives in the resource path; the listing carries it on each entry so the entries are addressable. Server-assigned and ignored on a write."
          },
          "stream": {
            "type": "string",
            "nullable": true,
            "description": "The stream's name. Server-assigned on a listing, like `application`; ignored on a write."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "This override's revision, to quote as `If-Match` on a write. Carried on the document; a read repeats it in the `ETag` header. Server-assigned and ignored on a write. It covers the config, not the identity members beside it, so the revision a listing carries matches the one the document's own resource answers."
          }
        }
      },
      "StreamGroupConfig": {
        "type": "object",
        "description": "Which streams this group applies to (its match rule: an application name and a\nstream-name pattern), and the config applied automatically to any of them that\nstarts.\n",
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[^.*?+^$|()\\[\\]{}\\\\]+$",
            "description": "The group's identity, and the base name of the file it is stored in. Immutable: a write that changes it is a 409, and the match rule can be rewritten without re-keying the group. `default` is reserved, and regex metacharacters are a `400` — the name doubles as a file name and would be silently rewritten."
          },
          "match": {
            "type": "object",
            "description": "Which streams are in this group.",
            "properties": {
              "application": {
                "type": "string"
              },
              "stream_pattern": {
                "type": "string",
                "description": "Stream name or pattern (e.g. `cam.*`)."
              },
              "priority": {
                "type": "integer",
                "default": 50,
                "description": "When several groups match, lower wins."
              }
            },
            "required": [
              "application",
              "stream_pattern"
            ]
          },
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The config applied to every stream the match rule selects."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "This group's revision, to quote as `If-Match` on a write. Carried on the document so a client that listed stream group configs can write one without reading it again; a single-group read repeats it in the `ETag` header. Server-assigned and ignored on a write."
          }
        },
        "required": [
          "name",
          "match"
        ]
      },
      "DefaultConfig": {
        "type": "object",
        "description": "The config every stream inherits, under everything else. The one stream group config with no match rule, because it matches every stream — which is also why its detector is a family of per-type baselines in `detectors` rather than one selected detector in `config`.",
        "properties": {
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BaselineConfig"
              }
            ],
            "description": "The baseline settings every stream inherits before any group or override applies."
          },
          "detectors": {
            "type": "object",
            "description": "Detector baselines for every type at once: a stream inherits the entry matching its own detector type. The default config selects no type, which is why these sit beside `config` rather than in it: `config.detector` selects one, and this document is the one that cannot.",
            "properties": {
              "scene": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SceneDetector"
                  }
                ]
              },
              "object": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ObjectDetector"
                  }
                ]
              },
              "vlm": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/VlmDetector"
                  }
                ]
              },
              "synthetic": {
                "nullable": true,
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SyntheticDetector"
                  }
                ]
              }
            }
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "This document's revision, to quote as `If-Match` on a write. Carried on the document; a read repeats it in the `ETag` header. Server-assigned and ignored on a write."
          },
          "concurrent_executions": {
            "type": "object",
            "description": "Parallel model instances per detector kind on VIS. The consumed kinds are `object_detection` and `scene_detection`; any other key is a `400` rather than silently dropped.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "default": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "additionalProperties": {
                "type": "integer",
                "minimum": 1
              }
            }
          }
        }
      },
      "ServerStatus": {
        "type": "object",
        "properties": {
          "host": {
            "allOf": [
              {
                "$ref": "#/components/schemas/HostStats"
              }
            ],
            "description": "Engine host and GPU statistics."
          },
          "streams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Stream"
            },
            "description": "Every stream currently running under video intelligence."
          },
          "vis_instances": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VisInstanceStatus"
            },
            "description": "Every VIS instance the Engine has connected to."
          }
        },
        "required": [
          "host",
          "streams",
          "vis_instances"
        ]
      },
      "HostStats": {
        "type": "object",
        "properties": {
          "wse_version": {
            "type": "string",
            "description": "Wowza Streaming Engine version."
          },
          "vif_module_version": {
            "type": "string",
            "description": "Installed VIF module version."
          },
          "cpu_avg": {
            "type": "number",
            "nullable": true,
            "description": "Engine host CPU load as a percentage, averaged over the last 5 samples."
          },
          "gpu": {
            "type": "object",
            "nullable": true,
            "description": "Engine host GPU stats; null when the host has no GPU or the driver cannot be read.",
            "properties": {
              "model": {
                "type": "string",
                "nullable": true
              },
              "driver_version": {
                "type": "string",
                "nullable": true
              },
              "cuda_version": {
                "type": "string",
                "nullable": true
              },
              "utilization_avg": {
                "type": "number",
                "nullable": true
              },
              "memory_avg": {
                "type": "number",
                "nullable": true
              },
              "encode_avg": {
                "type": "number",
                "nullable": true
              },
              "decode_avg": {
                "type": "number",
                "nullable": true
              }
            }
          }
        }
      },
      "VisInstanceStatus": {
        "type": "object",
        "properties": {
          "host": {
            "type": "string",
            "description": "VIS instance hostname or address."
          },
          "url": {
            "type": "string",
            "description": "VIS WebSocket endpoint this instance is reached at."
          },
          "version": {
            "type": "string",
            "nullable": true,
            "description": "The instance's reported software version."
          },
          "reachable": {
            "type": "boolean",
            "description": "Whether the Engine could reach this instance on the last check."
          },
          "age_seconds": {
            "type": "number",
            "nullable": true,
            "description": "Seconds since this instance was last reached. It ages the figures reported here: while `reachable` is false they are the last good reading, not current ones."
          },
          "cpu_pct": {
            "type": "number",
            "nullable": true,
            "description": "Instance CPU load as a percentage. Null means the instance reports no CPU gauge, which is not the same as 0."
          },
          "streams": {
            "type": "integer",
            "nullable": true,
            "description": "How many streams this instance is analyzing. A count, unlike the `streams` of `ServerStatus`, which is the streams themselves."
          },
          "stream_names": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "The streams behind the count, as `application/stream` names — what a dashboard renders next to the instance."
          },
          "gpus": {
            "type": "array",
            "nullable": true,
            "description": "GPUs on this instance; null when it reports none.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string",
                  "nullable": true
                },
                "utilization_pct": {
                  "type": "number",
                  "nullable": true
                },
                "memory_used_mb": {
                  "type": "number",
                  "nullable": true
                },
                "memory_total_mb": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          }
        },
        "required": [
          "host",
          "url",
          "reachable"
        ]
      },
      "ModelCatalog": {
        "type": "object",
        "properties": {
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetectionModel"
            },
            "description": "Detection models available on the connected VIS instances."
          },
          "vlm_defaults": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "The VLM prompt/schema defaults reported by the VIS instance the default config names, relayed verbatim — VIS owns the shape, and a config UI shows these as the effective standalone-VLM defaults. Null when VIS is unreachable or reports none."
          }
        },
        "required": [
          "models"
        ]
      },
      "ListenerType": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Simple Java class name, the value a custom listener's `class_name` takes."
          },
          "version": {
            "type": "string",
            "nullable": true,
            "description": "The version the implementation reports; a placeholder when it cannot say."
          }
        },
        "required": [
          "name"
        ]
      },
      "DetectionModel": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Model name, as passed to the object detector's `model` or `checkpoint_path`."
          },
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectorType"
              }
            ],
            "description": "Which detector this model works with."
          },
          "custom": {
            "type": "boolean",
            "description": "True for a custom checkpoint, false for a built-in variant."
          },
          "checkpoint_path": {
            "type": "string",
            "nullable": true,
            "description": "Path on the VIS host; pass as the object detector's `checkpoint_path`."
          },
          "class_set": {
            "type": "string",
            "nullable": true,
            "description": "Named class set (e.g. `coco`) when `classes` is not inlined."
          },
          "classes": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Classes this model can detect, when not named by `class_set`."
          }
        },
        "required": [
          "name",
          "type"
        ]
      },
      "VlmEndpointProbeRequest": {
        "type": "object",
        "description": "Probe target: a URL, a reference (`stream_group_config`, or `application`+`stream`), or both. An explicit `api_key` always wins. The reference's stored key is used only when no explicit `url` is given — i.e. only against the reference's own stored URL; an explicit `url` is never probed with the reference's stored key, so a stored key is never sent to a caller-supplied host.",
        "properties": {
          "url": {
            "type": "string",
            "nullable": true,
            "description": "OpenAI-compatible base URL; absent or null uses the reference's effective URL, and then its stored key unless an explicit `api_key` is given. When present, the reference's stored key is not used."
          },
          "api_key": {
            "type": "string",
            "nullable": true,
            "description": "Explicit key to probe with; wins over any reference's stored key."
          },
          "stream_group_config": {
            "type": "string",
            "nullable": true,
            "description": "A stored stream group config by name, whose effective VLM endpoint is the probe target."
          },
          "application": {
            "type": "string",
            "nullable": true,
            "description": "Engine application of the stream to probe, paired with `stream`."
          },
          "stream": {
            "type": "string",
            "nullable": true,
            "description": "Name of the running stream whose effective VLM endpoint is the probe target."
          }
        }
      },
      "VlmEndpointProbeResult": {
        "type": "object",
        "properties": {
          "reachable": {
            "type": "boolean",
            "description": "Whether the endpoint answered."
          },
          "models": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            },
            "description": "Model names the endpoint reports; null when it does not answer or offers none."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Why the probe failed, when it did."
          }
        },
        "required": [
          "reachable"
        ]
      },
      "VodFile": {
        "type": "object",
        "description": "One file under the content directory, as a job request names it.",
        "properties": {
          "file": {
            "type": "string",
            "description": "Path relative to the content directory, with forward slashes — post it back verbatim as a job's `file`."
          },
          "size_bytes": {
            "type": "integer",
            "format": "int64",
            "description": "File size in bytes."
          },
          "modified_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last modification time, RFC 3339 with a UTC offset."
          }
        },
        "required": [
          "file",
          "size_bytes",
          "modified_at"
        ]
      },
      "VodFileList": {
        "type": "object",
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VodFile"
            },
            "description": "The analyzable files, newest first."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the listing was cut off at its maximum of 500 entries."
          }
        },
        "required": [
          "files",
          "truncated"
        ]
      },
      "VodJobState": {
        "type": "string",
        "enum": [
          "pending",
          "connecting",
          "running",
          "completed",
          "failed",
          "cancelled"
        ],
        "description": "Where a job is: queued, connecting to the analysis service, analyzing, or in one of the three states it never leaves. Filters accept these names in any case."
      },
      "VodFailureCause": {
        "type": "string",
        "enum": [
          "response_timeout",
          "disconnected",
          "detector_restarted",
          "send_failed",
          "endpoint_degraded",
          "not_connected",
          "connect_failed",
          "detector_error",
          "config_drift",
          "coverage_shortfall",
          "source_error",
          "store_error",
          "engine_restart"
        ],
        "description": "Why a job failed, as a class a client can act on; `error` carries the words."
      },
      "VodJobRequest": {
        "type": "object",
        "description": "A submission. Beside `file`, at least one of `stream_group_config` and `config` names the configuration; the rest tune how the job is kept and reported. A `template` member is reserved for detector profiles and is not accepted yet.",
        "properties": {
          "file": {
            "type": "string",
            "description": "The source, relative to the content directory, as `GET /vod/files` lists it."
          },
          "stream_group_config": {
            "type": "string",
            "nullable": true,
            "description": "A stream group config by its `name`. Its config is the middle layer, over the default config; its match rule is not consulted. Pre-v2 configuration files answer to their file name without the extension."
          },
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "An inline config; the top layer over the default config or the named stream group config."
          },
          "store_results": {
            "type": "boolean",
            "default": true,
            "description": "Whether to keep the job's detections for `GET .../results`. `false` runs the job status-only, which needs a listener that can run offline."
          },
          "tag": {
            "type": "string",
            "nullable": true,
            "description": "A free-form label to group jobs by; the listing filters on it exactly."
          },
          "lifecycle_webhook": {
            "type": "string",
            "nullable": true,
            "description": "Where to POST this job's state changes, overriding the VOD settings' destination. Absent inherits that destination; `\"\"` turns notifications off for this job. The configured secret is sent only to the configured destination, never to one given here — authorize a destination of this job's own by naming a secret in `lifecycle_webhook_secret`."
          },
          "lifecycle_webhook_secret": {
            "type": "string",
            "nullable": true,
            "description": "The name of a configured secret whose value goes out as the `Authorization` header on this job's notifications. A name nothing configures is refused at submit."
          },
          "auto_resume": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether to resume this job automatically when it stops for a transient reason, overriding the VOD settings' default (on)."
          }
        },
        "required": [
          "file"
        ]
      },
      "VodJob": {
        "type": "object",
        "description": "A job as this Engine knows it. The same document is a lifecycle-webhook payload, with `event: \"status_changed\"` and `results` (a server-relative path to the job's results) added; a `POST /vod/jobs` and the listing leave out `config` and `effective_config`, which the single-job read carries.",
        "properties": {
          "job_id": {
            "type": "string",
            "description": "The job's id, assigned at submission."
          },
          "file": {
            "type": "string",
            "description": "The source file, relative to the content directory."
          },
          "tag": {
            "type": "string",
            "nullable": true,
            "description": "The free-form label the job was submitted with, if any."
          },
          "detector_type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectorType"
              }
            ],
            "description": "The kind of analysis this job runs."
          },
          "stream_group_config": {
            "type": "string",
            "nullable": true,
            "description": "The stream group config the job was submitted with, by name; null for an inline-only job."
          },
          "listener_warning": {
            "type": "string",
            "nullable": true,
            "description": "The configured listeners a file job cannot serve (overlay, ID3) and skipped."
          },
          "store_results": {
            "type": "boolean",
            "description": "Whether the job's detections are kept for `GET .../results`."
          },
          "results_truncated": {
            "type": "boolean",
            "description": "True when a write failure cut the stored results short of the file's end; a resume fills the gap."
          },
          "state": {
            "allOf": [
              {
                "$ref": "#/components/schemas/VodJobState"
              }
            ],
            "description": "Where the job is in its lifecycle."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Failure detail, in words."
          },
          "error_cause": {
            "allOf": [
              {
                "$ref": "#/components/schemas/VodFailureCause"
              }
            ],
            "description": "Why the job failed, as a class a client can act on."
          },
          "requests_sent": {
            "type": "integer",
            "format": "int64",
            "description": "Analysis requests answered so far."
          },
          "requests_total": {
            "type": "integer",
            "format": "int64",
            "description": "A ceiling estimate of the requests the file takes; `requests_sent` may land one short of it."
          },
          "media_time_ms": {
            "type": "integer",
            "format": "int64",
            "description": "The media position the analysis has reached."
          },
          "source_duration_ms": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "The file's duration, when the container could be probed."
          },
          "queued_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the job was submitted."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When analysis began; null before it starts."
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the job reached a terminal state; null while it is still queued or running."
          },
          "resumes": {
            "type": "integer",
            "description": "How many times the job was resumed."
          },
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The configuration the job was submitted with."
          },
          "effective_config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The configuration the job actually ran with, fully resolved."
          }
        },
        "required": [
          "job_id",
          "file",
          "store_results",
          "results_truncated",
          "state",
          "requests_sent",
          "requests_total",
          "media_time_ms",
          "queued_at",
          "resumes"
        ]
      },
      "VodJobPage": {
        "type": "object",
        "properties": {
          "offset": {
            "type": "integer",
            "description": "How many jobs were skipped before this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size requested."
          },
          "count": {
            "type": "integer",
            "description": "Jobs in this page."
          },
          "total": {
            "type": "integer",
            "description": "Jobs matching the filters, across every page."
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VodJob"
            },
            "description": "This page's jobs, newest first."
          }
        },
        "required": [
          "offset",
          "limit",
          "count",
          "total",
          "jobs"
        ]
      },
      "VodJobResumeRequest": {
        "type": "object",
        "description": "What a resume may carry: the inline `config` of a job whose credentials were redacted from its record, so the identical analysis can run with live credentials. Empty for every other job.",
        "properties": {
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Config"
              }
            ],
            "description": "The inline config to resume with, for a job whose credentials were redacted from its record."
          }
        }
      },
      "VodResultsPage": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string",
            "description": "The job these results belong to."
          },
          "offset": {
            "type": "integer",
            "description": "How many rows were skipped before this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size requested."
          },
          "count": {
            "type": "integer",
            "description": "Rows in this page."
          },
          "total": {
            "type": "integer",
            "description": "Rows matching the filters, across every page."
          },
          "results": {
            "type": "array",
            "description": "The stored rows, verbatim — each the analysis service's own response document.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "required": [
          "job_id",
          "offset",
          "limit",
          "count",
          "total",
          "results"
        ]
      },
      "VodSettings": {
        "type": "object",
        "description": "How this Engine runs on-demand analysis. Every member has a built-in default; an absent member means it.",
        "properties": {
          "max_concurrent_jobs": {
            "type": "integer",
            "minimum": 1,
            "description": "Jobs analyzed at once; the rest queue. Default 1."
          },
          "max_jobs": {
            "type": "integer",
            "minimum": 1,
            "description": "Job records kept; the oldest finished job is evicted first. Default 25."
          },
          "job_ttl_seconds": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "How long a finished job stays before retention removes it; 0 keeps them until evicted. Default 0."
          },
          "content_dir": {
            "type": "string",
            "nullable": true,
            "description": "The content directory jobs read from and uploads land in, relative to the Engine home unless absolute. Applies at the next Engine start."
          },
          "jobs_dir": {
            "type": "string",
            "nullable": true,
            "description": "Where job records, results and thumbnails are stored. Applies at the next Engine start."
          },
          "lifecycle_webhook": {
            "type": "string",
            "nullable": true,
            "description": "The default destination of every job's state changes; a job may override or turn it off."
          },
          "lifecycle_webhook_secret": {
            "type": "string",
            "nullable": true,
            "description": "The name of the secret sent as `Authorization` to the default destination — and only to it."
          },
          "auto_resume": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether a job that stops for a transient reason is resumed on its own. Default true."
          },
          "max_upload_bytes": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "The largest upload `POST /vod/files` accepts; beyond it, `413`. Default 10 GiB."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "Server-assigned and ignored on a write."
          }
        }
      },
      "Secrets": {
        "type": "object",
        "description": "The configured secrets: their names on the way out, their values on the way in. A read never carries a value; the revision is computed over the values, so a rotation is visible as a new `ETag`.",
        "properties": {
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The configured secret names, sorted."
          },
          "values": {
            "type": "object",
            "writeOnly": true,
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "The merge patch — a string sets or rotates, `null` removes, an absent name is kept."
          },
          "etag": {
            "type": "string",
            "nullable": true,
            "description": "Server-assigned and ignored on a write."
          }
        }
      }
    }
  }
}